# Marionette 5.0.0 documentation bundle Version: 5.0.0 Channel: latest Publication: stable release (published on npm) Source revision: 0f2284ad4bbfe8c82ce82714471697a68c2acaeb Local changes: false Content SHA-256: 44c0d535fd36d11f31d909246f4ee7c6592eae3f25fb7513c61af65842593a90 Read individual documents for focused tasks. Each section below contains the exact published reading Markdown, including its source identity. --- Document: docs/guides/framework-migration.md Canonical URL: https://marionettejs.com/docs/guides/framework-migration/ Markdown URL: https://marionettejs.com/docs/guides/framework-migration.md Reading SHA-256: b28384f7a8cc0397c46b4e6b65206e2e58cb9cd92dc55ee204360bb6d7ec29c2 # Migrate from another UI framework Preserve the application's observable behavior while designing a native Marionette ownership tree. Translating each Vue component, React hook, or store into a similar Marionette file can preserve the old architecture's problems. Decide who owns data, UI, readiness, and mutations before choosing the order of implementation. This guide targets **5.0.0**. Use [matching packages](/docs/quick-start.md#install-matching-packages) and their installed contracts. For an existing Marionette v4 app, use the separate [v4 migration guide](/docs/guides/migration.md). ## 1. Inventory behavior before implementation Build a journey matrix from the running original app and its public contracts. For each journey, record the entry URL, role/permissions, inputs, visible outcomes, API methods/payloads, failure cases, and evidence. Include direct links, navigation, search/filter/sort, edits, uploads, empty states, keyboard/focus, persistence after reload, and work interrupted by navigation. Identify cross-feature behavior such as a rename appearing in both a list and an open detail panel. Keep known baseline bugs explicit: record the reproduction and decide whether to preserve it temporarily or fix it intentionally with a changed expectation. A failing original journey is evidence, not permission to omit the feature. Separate observed behavior from intended behavior where they differ. Run the original through its **public UI against a controlled backend** before building the replacement. Reuse those same journey tests against the replacement by changing its base URL and startup configuration. Prefer accessible roles, labels, URLs, visible state, and recorded API contracts. Avoid Vue component instances, React internals, store getters, private Marionette fields, or selectors that encode the old component tree. A small public test-id contract can identify controls without making their component layout part of the specification. Control test accounts, permissions, fixture reset, request delays, rejection, and out-of-order completion. Check that a successful edit survives reload and that forbidden edits fail at the backend as well as being unavailable in the UI. Keep backend contract tests alongside browser journeys; local mocks cannot prove server permissions or real payload compatibility. Review test changes separately: do not weaken assertions or add skips to make the replacement pass. Deliberately break a representative response or interaction and confirm the test fails. ## 2. Draw the final ownership tree independently Use [architecture](/docs/architecture.md) to define the target tree from domain lifetimes and user journeys. The old source is evidence for business rules and edge cases; its folders and component boundaries do not decide the final tree. For a record workspace, distinguish lifetime ownership from screen placement: ```text Lifetime owners WorkspaceApplication ├─ ShellView — persistent root UI └─ registered RecordApplication ├─ draft — shared workflow state └─ RecordPageView — feature root UI RecordService — transport, serialization and persistence contracts Screen placement ShellView └─ destination Region ← RecordApplication.showView() └─ RecordPageView ├─ editor Region → TitleEditorView (borrows draft) └─ summary Region → SummaryView (latest loaded title) ``` WorkspaceApplication registers RecordApplication and explicitly starts/stops it; registration alone neither starts nor awaits a child. ShellView owns the destination Region used to place the feature's root View. Emptying that Region removes UI but does not stop the Application or end its ongoing authority: the feature owner must stop or destroy it when ending the run. After showing the ShellView instance `shell`, pass `shell.getRegion('destination')` as `region` to the child instance's `start({ region, id })`. The [asynchronous routing example](/docs/guides/routing.md#route-to-asynchronous-features) shows the complete shell-to-child handoff; [child Application ownership](/docs/api/application.md#child-applications) explains registration and cleanup. An Application coordinates a feature lifetime; a View owns presentation and local interaction; a Region places and disposes its child. Use CollectionView for repeated records that need independent children. A View can call its model/API layer to save a local edit; an async call alone does not justify an Application. Put shared workflow decisions and work that survives panel replacement with a surviving owner. For every source or operation, record: | Decision | Example | | --- | --- | | Owner and borrowers | RecordApplication owns its draft; editor borrows it. A shared record cache belongs to the session/service lifetime. | | Mutation authority | Editor updates draft title; feature coordinates publishing; service persists. Siblings emit intent instead of editing each other's DOM. | | Readiness | `prepareStart(options, { signal })` loads required data; `onStart` applies only the current result. | | Retention | Restart preserves shell/input; Region replacement destroys its outgoing View; stop destroys root UI but retains Application state. | | Cancellation and release | Pass preparation signal to transport. Give ongoing saves their own cancellation/concurrency policy and end external effects at their owner. | `start()` and `restart()` resolve `true` on success, `false` when superseded or cancelled, or when a stopping/terminal owner prevents activation. Current preparation or startup-callback failures reject; synchronous failures do not roll back completed work. `stop()` and `destroy()` are synchronous; notification hooks do not await Promises. The preparation signal ends with that operation, so it is not an ongoing feature-lifetime signal. Avoid application side effects during preparation that could commit after cancellation. See [Application](/docs/api/application.md), [retained restart](/docs/guides/retained-restart.md), and [routing](/docs/guides/routing.md) for complete lifecycle/error boundaries. A supplied state source is borrowed; `createState()` establishes owned state. Declare what survives panel replacement, route exit, stop, and reload separately. Choose one writer or an explicit conflict policy for each shared mutation. Model observation and server persistence are separate capabilities: optional [@mnjs/data](/docs/packages/data.md) has no `Model.save()` or `fetch()` contract. Do not assume Backbone methods. ## 3. Choose the migration strategy deliberately A replacement branch can have incomplete routes while the correct architecture is being built. Full parity is the final goal; keeping a hybrid app functional at every intermediate checkpoint is not a default requirement. Preserve the runnable original as the baseline and record unavailable replacement journeys honestly. Choose **leaf-first** when a leaf has a clean DOM/data boundary and its replacement will be reused unchanged in the target tree. A date picker or independent panel may qualify. Do not start there merely because leaves are small: dependencies on old stores, parent effects, or component conventions can turn every leaf into a compatibility project. Build those leaves inside the replacement shell when coexistence is unnecessary; hosting a leaf in the running original requires the bridge ownership and deletion criteria below. Choose a **native feature-family replacement** when the old tree obscures ownership or when no production coexistence is required. Establish the shell, service boundary, and one representative journey, then migrate related behavior in broad batches: record list/filter/detail; editing/validation/persistence; permission-sensitive commands; navigation/recovery. Each batch includes its data, UI, effects and tests, rather than converting files individually. This avoids repeatedly inventing the same authority and cleanup rules across dozens of components. Pilot a representative risk early: an editable record with a shared list/detail, a permission boundary, delayed readiness, navigation away, and retry. Use it to challenge the ownership plan before scaling. A trivial static page is useful for installation, but does not validate migration architecture. If production must run both frameworks, use [existing UI](/docs/guides/existing-ui.md) for an explicit host boundary: one owner per DOM subtree, public inputs/events, cleanup before removing the mount, and no sibling DOM access. Record the bridge owner, why coexistence is required, the consuming routes, and the concrete condition for deleting the bridge and its dependencies. Keep the bridge at that boundary rather than teaching every new View the old framework's store or lifecycle. ## 4. Work through a native ownership example This invented Vue-like sketch combines readiness, UI and draft authority in a component. It is illustrative pseudocode, not an executable Vue recipe: ```text RecordPage.vue setup(): obtain route id and global store watch(route.id): fetch record, overwrite store, reset form component state: title draft and request/loading flags template: title editor + summary + reload button child callbacks: write global store and query parent's DOM unmount(): cancel component request ``` The native replacement below separates readiness from local interaction. It uses Lit for safe text interpolation and stable input updates, and observable state from `@mnjs/data`. Follow [setup](/docs/integrations/setup.md) for matching companion packages. `GET /records/42.json` returns `{ "title": string }`; API access stays in `loadRecord`, which can move into the application's service module. See [production deployment](/docs/guides/production.md#serve-direct-links-deliberately) for HTTPS and direct-link requirements. ```js import { Application, View } from 'marionette'; import { Model, DataApi, StateApi } from '@mnjs/data'; import LitDomApi from '@mnjs/adapters/dom/lit-html'; import { html } from 'lit-html'; import { live } from 'lit-html/directives/live.js'; async function loadRecord(id, signal) { const response = await fetch(`/records/${encodeURIComponent(id)}.json`, { signal }); if (!response.ok) throw new Error('Could not load record.'); return response.json(); } const PageView = View.extend({ template: () => html`
`, regions: { editor: '.editor', summary: '.summary' }, triggers: { 'click .reload': 'reload' }, }).setDomApi(LitDomApi); const TitleEditorView = View.extend({ template: ({ title }) => html` `, ui: { title: '.title' }, events: { 'input @ui.title': 'editTitle' }, modelEvents: { 'change:title': 'render' }, editTitle() { this.model.set('title', this.getUI('title')[0].value); }, }).setDataApi(DataApi).setDomApi(LitDomApi); const SummaryView = View.extend({ template: ({ loadedTitle, error }) => html`

Loaded title: ${loadedTitle}

${error}

`, modelEvents: { 'change:loadedTitle': 'render', 'change:error': 'render' }, }).setDataApi(DataApi).setDomApi(LitDomApi); const RecordApplication = Application.extend({ createState() { return new Model({ title: '', loadedTitle: '', error: '' }); }, viewEvents: { reload: 'reloadRecord' }, prepareStart({ id }, { signal }) { return loadRecord(id, signal); }, onStart(_app, { id }, record) { this.recordId = id; const state = this.getState(); state.set({ loadedTitle: record.title, error: '' }); if (this.getView()) return; state.set('title', record.title); const page = this.setView(new PageView()); page.showChildView('editor', new TitleEditorView({ model: state })); page.showChildView('summary', new SummaryView({ model: state })); this.showView(); }, reloadRecord() { return this.restart({ id: this.recordId }).catch(error => { this.getState().set('error', error.message); }); }, }).setStateApi(StateApi); const mount = document.createElement('main'); document.body.append(mount); const app = new RecordApplication({ region: { el: mount } }); await app.start({ id: '42' }); ``` Reload repeats readiness for the **same record**. Only SummaryView updates; the page and editor stay owned by their Regions, and unfinished input survives. The editor's `ui` map gives its owned control one name instead of repeated `querySelector` calls. Read `getUI('title')[0]` after binding; bindings rebind on render and are snapshots, so do not keep a stale element across replacement. The editor observes title changes so external updates also appear. Its rerender retains the input through the chosen Lit integration and `live()` binding; verify focus and caret behavior before substituting another renderer. Calling `start({ id: next })` on an active Application resolves `true` without repeating preparation or applying the new options. This example supports restart only for its current record; a different-id restart would retain the old draft beside the new loaded title. A route to another record needs a deliberate draft policy: retain drafts by id, ask before discarding, or stop/start to reconstruct. Do not use this same-record reload as an implicit navigation policy. This example resets the draft from loaded data after stop/start, although the Application retains its state source. Preserving drafts across reconstruction requires an explicit policy, such as drafts keyed by id. For initial readiness failure, the caller handles rejection and stops the failed run before error presentation; the [routing example](/docs/guides/routing.md#route-to-asynchronous-features) supplies that boundary. `reloadRecord` reports current lifecycle rejections, including startup-callback failures; it is not a load-only error classifier. Reporting feedback does not roll back partial updates or establish safe recovery from callback bugs. Follow the [Application failure contract](/docs/api/application.md#preparation-cancellation-and-failure) when deciding the application's recovery and diagnostic policy. This example has no save operation: adding one requires the service's write contract and a decision about cancellation, duplicate submissions, conflict, and which surviving owner receives completion. See [local editing](/docs/guides/local-editing.md). ## 5. Use separate architecture and behavior gates At each feature-family checkpoint, review **source** for the intended ownership, mutation authority, cancellation, stable UI bindings, and removed compatibility paths. Then run the reusable **behavior tests** against the integrated replacement. Passing interactions can hide a replacement that still delegates to the old app; a clean source tree can still break a journey. Neither gate replaces the other. Keep one short checkpoint record in the application's existing project notes: ```text Batch: record editing Decisions: feature owns draft; service persists; same-record reload retains editor Evidence: exact revision/packages, commands, environment, journey results Open failures: permission revocation during save; unimplemented bulk edit route Coverage boundary: real backend tested for save; conflict path still mocked Next: resolve conflict policy, complete bulk edit, rerun list/detail/save together ``` Exercise true integration across feature families: direct URL → readiness → edit → save → list/detail update → navigation → reload; also auth expiry and permission changes, rapid route changes, slow responses, stop/start, and repeated attachment where widgets/listeners depend on it. Use a real browser for focus, keyboard, history and editable-state behavior. Component tests and screenshots support this coverage but do not establish it. Report baseline failures separately from new regressions and explicitly unimplemented routes. Test counts and converted file counts are progress measures, never completion claims. ## 6. Audit the final replacement The replacement is complete when every required journey passes its recorded expectation, updated for documented intentional changes, and every baseline bug has a recorded disposition. No required route remains unimplemented; checkpoint failures are resolved or recorded as intentional behavior changes. Re-run the full journey matrix and backend contract checks against the production entrypoint and controlled backend, including the interruption and failure cases. Review changed expectations independently from implementation. Audit source imports, dynamic imports, route lazy chunks, build plugins, manifests/lockfile, and the actual generated bundle/module graph. Remove the old framework, adapters, stores, bridges, obsolete entrypoints, unused styles, and dependencies when no required consumer remains. A green suite does not prove removal; verify the new app cannot reach or invoke the old implementation, including through lazy routes. Preserve applicable public licenses and attribution for reused source/assets. Use public examples or authorized project code; do not copy private application source into public documentation or fixtures. The public [Marionette RealWorld example at a pinned revision](https://github.com/marionettejs/marionette-realworld-example-app/tree/34ceaa5987c1e80dfa6b6f2285cb9fcb5296ce04) is a reference to inspect, not proof of parity for another application. ## Agent checklist - Pin original/replacement revisions, package contract, backend fixtures and known bugs. - Run portable journey tests on the original; check their ability to catch breakage. - Define target owners, borrowers, writers and cancellation before converting code. - Choose leaf-first or native feature batches with a reason; document bridge deletion only if needed. - Pilot representative risk, then complete related behavior in broad batches. - Review architecture and test changes separately from integrated behavior results. - Record decisions, exact evidence, incomplete routes and open failures at checkpoints. - Complete every required journey against recorded expectations, documenting intentional changes; resolve open failures and prove old-framework removal in imports, dependencies and bundles. ## Further reading [Playwright best practices](https://playwright.dev/docs/best-practices) covers user-facing locators, isolated test state, controlled data, and retrying assertions for the reusable browser journeys. [Addy Osmani's Brownfield Agentic Engineering](https://addyosmani.com/blog/brownfield-agentic-engineering/) explains characterization, durable context, and completing migration units with old dependencies removed. [Anthropic's migration guide](https://claude.com/blog/ai-code-migration) describes a portable behavior judge, checking that it detects broken behavior, and distinguishing translation from architectural redesign. Apply those verification principles to UI journeys while using Marionette's installed contracts for the replacement architecture. [Canonical source](/docs/markdown/docs/guides/framework-migration.md) · [Source identity](/docs/manifest.json) --- Document: docs/guides/migration.md Canonical URL: https://marionettejs.com/docs/guides/migration/ Markdown URL: https://marionettejs.com/docs/guides/migration.md Reading SHA-256: 6b7c3cab3bd08d8323a445af2180bf35f69273e4f9fb7fd256240ed965c88db5 # Migrate from v4 to v5 Upgrade one application and its messaging participants together. Start from a working v4 application with checks for its important interactions, then change installation and integration setup before changing feature ownership. This guide covers common v4 application code; custom providers and low-level overrides should also be checked against their [reference contracts](/docs/api.md). For an agent-assisted upgrade, give the agent this guide and the [agent entrypoint](/docs/agents.md), plus the application's entrypoint and existing behavior checks. Have it inspect the installed package's docs, migrate one representative feature, and run the checks in section 4 before repeating the changes elsewhere. Keep data-layer replacement separate from the framework upgrade unless the application needs both. ## 1. Replace installation and configure integrations Replace `backbone.marionette` with the matching `marionette` v5 release. Follow [package installation](/docs/quick-start.md#install-matching-packages) for the current package artifacts and companion versions. If you already use named imports, change their package path. Replace default namespace imports with named imports: `import { Application, View, MnObject, Radio } from 'marionette'`. The former namespace's `Object` alias becomes `MnObject`. Core supplies native DOM operations and accepts plain objects and static arrays. Backbone, jQuery, and Underscore are no longer required by core. Remove a dependency only when the application itself no longer uses it. Configure the data sources and rendering your application actually uses in one setup module, imported before constructing consumers: - An existing Backbone application can retain its Models, Collections, and persistence operations. Import `BackboneApi` from `@mnjs/adapters/backbone`, then call `setDataApi(BackboneApi)`. Call `setStateApi(BackboneApi)` separately when owners use Backbone state. This connects existing data without changing Backbone objects or their native methods. - For new observable data, [@mnjs/data](/docs/packages/data.md) provides local Models and Collections. Use its `DataApi` and, when needed, `StateApi` as described in [setup](/docs/integrations/setup.md). Fetching and persistence remain separate concerns. Replacing Backbone's `save`, `fetch`, or server-side `destroy` requires a deliberate data-layer change. - The default renderer calls a template function with its data; it does not compile template selector strings. Preserve an application template compiler through `setRenderer` when needed. For Lit templates, configure `LitDomApi` from `@mnjs/adapters/dom/lit-html` with `setDomApi`. - Code requiring jQuery query or content methods can explicitly select `JQueryDomApi` from `@mnjs/adapters/dom/jquery`. This adapter does not create `$el` or restore jQuery event delegation. Its peer requirement is jQuery 4; verify application plugins before selecting it. See [adapters](/docs/packages/adapters.md) for installation, provider behavior, and tested peers. Ordinary applications can use the default runtime setters; [runtime configuration](/docs/api/runtime.md) also supports class-specific setup and isolation. Replace every `backbone.radio` import with the selected runtime's Radio in the same upgrade, including publishers and requesters outside Marionette classes. The old and new buses have independent channels, so leaving either participant on the old bus disconnects communication. Default-runtime code can import `Radio` from `marionette`; isolated runtime code must use that runtime's `Radio`. Replace `Radio.DEBUG = true` with `Radio.setDebug()`. `Radio.log` and `Radio.debugLog` remain assignable logging hooks. See [Radio](/docs/packages/radio.md) for registry scope and request/reply methods. ### Import and configure existing Backbone data This upgrade retains Backbone persistence. Change every Radio participant together: ```diff -import Mn from 'backbone.marionette'; -import Radio from 'backbone.radio'; +import { View, Radio, setDataApi } from 'marionette'; +import BackboneApi from '@mnjs/adapters/backbone'; +setDataApi(BackboneApi); -const Editor = Mn.View.extend({ modelEvents: { change: 'render' } }); +const Editor = View.extend({ modelEvents: { change: 'render' } }); ``` ### Replace jQuery event assumptions Native delegated events require explicit cancellation. `getUI()` still returns a collection, so select its element before using DOM properties: ```diff onSave(event) { - const title = this.getUI('title').val(); + event.preventDefault(); + const title = this.getUI('title')[0].value; this.model.save({ title }); - return false; } ``` This example assumes the Backbone provider above and a single named input. Add `event.stopPropagation()` when stopping propagation was part of the intended v4 behavior; cancelling a form submission alone does not require it. ## 2. Update Views and composition The root remains fixed for each View's lifetime. Resolve selector strings to actual elements before supplying `el`; unwrap an existing jQuery element with `[0]`. To use a different root, destroy the old View and construct a new one. Regions still accept selector strings as mount points. | Existing code or assumption | v5 change | | --- | --- | | `setElement(...)`, inherited `remove()` | Construct with the intended `el`; use `destroy()` for complete teardown. | | `$el`, jQuery methods on `ui.name`, `getUI(...)`, or `this.$(selector)` | Native UI queries return array-like collections. Use `this.getUI('name')[0]` for one element. With the jQuery DOM adapter, UI queries return jQuery collections; create `$(this.el)` explicitly if the application needs a root wrapper. | | jQuery delegated events, `return false`, event namespaces | Native handlers receive a DOM event. Use `event.delegateTarget` for a selector match and explicit `preventDefault()` / `stopPropagation()` when needed. Core does not inherit singular `delegate` / `undelegate` helpers. | | `children.findByModelCid(model.cid)` | Use `children.findByModel(model)`. For an explicit provider key, use `findByKey(key)`; keys come from the configured DataApi, not necessarily a Backbone `cid`. | | `viewComparator: false` to ignore collection reordering ([v4.1.3 example](https://github.com/marionettejs/backbone.marionette/blob/v4.1.3/docs/marionette.collectionview.md#defining-the-viewcomparator)) | Also set `sortWithCollection: false`; disabling the comparator alone does not disable v5 source reorder notifications. | | Collection-only templates reading `items` | Read `models`, the array of serialized model values. This change applies to a View's collection template data. | | `region.show(stringOrOptions)` or `showChildView(name, template)` | Construct the intended View explicitly and pass its instance. | | `getRegion`, `getRegions`, or `hasRegion` implicitly rendering a layout | These are inspection methods. Use `showChildView` for composition, or explicitly render before showing directly through a selector Region. | | View Region registration/removal events | Put setup alongside registration and observe the Region's own destruction when needed. The View's `add:region` / `remove:region` notifications are removed. | | Global `setEnabled` / `isEnabled` calls | Remove the registry. Configure `childViewEventPrefix` on the View and `preventDefault` / `stopPropagation` on individual trigger definitions. Move application settings to explicit configuration or state. | | Lifecycle hooks supplied as constructor options | Define hooks on the class or instance. `triggerMethod` resolves instance/prototype methods. | | `bind` / `unbind`, or space-separated framework event names | Use `on` / `off`. Each event or request name is literal; use maps or separate calls for multiple names. Backbone sources retain their own native event semantics. | | Root imports of target-first utility wrappers | Prefer the object's method, or use the receiver-based functions from [@mnjs/utils](/docs/packages/utils.md). | The UI query and native DOM handler changes also apply to Behaviors; their element and delegated interactions belong to the host View. See [Behavior](/docs/api/behavior.md) for that contract. Keep root attributes distinct from template contents. `renderAttributes()` refreshes declared root attributes; `render()` refreshes contents. Native attribute removal requires `null`, and boolean HTML attributes should be present or absent rather than assigned `false`. Set live input values through DOM properties. See [View bindings](/docs/api/shared/view-bindings.md) for templates, attributes, UI, and events. For CollectionView customizations, `attachHtml(els, container)` receives a native container. The child container supports iteration and its documented helpers; replace removed Underscore aliases or iteratee shorthand with explicit callbacks/native array operations. Sorting and filtering retain their public methods and callback forms; verify the [CollectionView reference](/docs/api/collection-view.md) when overriding them. After changing a Model attribute used by a comparator or filter, call `list.sort()` or `list.filter()` explicitly. This applies to both `@mnjs/data` and Backbone Models. Filtering still detaches rows without destroying them, and `children` still describes the presented set. These are presentation operations, not collection membership changes. ## 3. Make feature readiness and ownership explicit In v4, `Application.start()` fired notifications synchronously on every call and returned the Application. v5 adds `stop()`, `restart()`, and `isRunning()`. Await `start()` and `restart()`: they return `Promise` and can reject. `stop()` and `destroy()` return synchronous booleans; remove Promise chaining and asynchronous teardown hooks. Starting an already-running Application does not repeat preparation. Handle failure at the boundary that requests the operation. Move work required before showing a feature from an async `onStart` or `onRender` into `prepareStart(options, { signal })`. Pass the signal to the request; return the ready result for `onStart(app, options, result)`. Notification hooks remain synchronous and their returned Promises are not awaited. The [Application reference](/docs/api/application.md#prepare-before-showing-ui) contains a complete readiness example and explains cancellation and failure. Use `onStart` to apply the ready result. Construct the shell and its connections once if it should survive restart; `viewEvents` observes root intent without rebinding on every start. When composing before attachment, call `setView(page)`, then the page's `showChildView(...)`, then `showView()`. The Application owns its selected root; stop destroys it, and a later start creates a new one. A Region built from constructor configuration is owned; a supplied Region instance is borrowed and remains reusable. Register child Applications through `childApps` or `addChildApp` when the parent should own their lifetime. Registration does not start them: request their start explicitly and handle that Promise. Parent stop stops registered children; parent destruction destroys them. Keep local input/model edits in their View when they need no feature coordination; Application is useful when readiness, shared workflow, or lifetime requires an owner. `restart()` repeats preparation without stopping the active run. Keep the root and its child Views in `onStart` when preserving focus and input; see [retained restart](/docs/guides/retained-restart.md). Restart leaves registered children active, so restart a child explicitly when its readiness must change too. Use `app.stop(); await app.start(options)` for reconstruction or host relocation. Finish required saves before calling synchronous stop/destroy. Ordinary `listenTo` subscriptions to surviving sources remain bound across stop and restart. ## 4. Verify the migrated application Check the installation and actual entrypoint first, then exercise representative behavior. The [consumer-testing guide](/docs/guides/testing.md) supplies runnable checks for interaction, replacement, readiness and teardown: 1. A successful start shows ready UI; a failed or superseded start does not commit obsolete UI. Verify that callers handle rejection. 2. Local edits and collection membership/order changes update through the chosen data provider. Existing Backbone persistence still uses its intended API. 3. Events and Radio requests reach every intended participant. Check event maps and former whitespace-separated names. 4. Replacing a panel destroys the old View while surviving shared data remains usable. Stopping and starting a feature recreates its root; restart preserves the intended controls through loading, failure, and the chosen successful update. 5. Successful owner destruction removes its UI and releases its subscriptions. A surviving listener receives the final `destroy` notification, then the destroyed Marionette source releases incoming listeners automatically. Extra destroy-to-`stopListening(source)` handlers are unnecessary for Marionette sources; external sources keep their own cleanup contracts. Remove manual root/registered-child destruction that duplicates Application ownership. Continue releasing application-owned timers, external subscriptions, and operations outside preparation according to their lifetime; framework cleanup cannot infer those resources. Use public behavior checks rather than private event registry assertions. See [event cleanup](/docs/api/shared/events.md#cleanup) and the class references when deciding who disposes a resource. [Canonical source](/docs/markdown/docs/guides/migration.md) · [Source identity](/docs/manifest.json) --- Document: docs/guides/testing.md Canonical URL: https://marionettejs.com/docs/guides/testing/ Markdown URL: https://marionettejs.com/docs/guides/testing.md Reading SHA-256: 152eafd28ab70b64eb2ad33d8272a00864552f0dd4e2f33701000eb79475dd96 # Test a consumer application Test what a user or owning component can observe: rendered content, interaction, readiness, and disposal. A local editing View needs no Application. An Application test is useful when a feature must prepare before showing its screen. This recipe uses Node's built-in test runner and JSDOM. Use a [supported Node LTS release](/docs/quick-start.md). First install `marionette`, `@mnjs/adapters`, `lit-html`, and `@mnjs/data` using the [package installation](/docs/quick-start.md) and [data setup](/docs/integrations/setup.md). Add the DOM implementation used here: ```sh npm install --save-dev --save-exact jsdom@30.1.0 ``` Create these four files in a `test` directory. The `.mjs` extension works without changing your application's module configuration. The example configures data and rendering on its own View classes to keep this recipe self-contained. Tests of your real classes should import your application setup module after the DOM preload and before those classes, as described in [integration setup](/docs/integrations/setup.md). `@mnjs/data` supplies optional observable attributes; API requests and persistence need a separate layer or another data solution. ## `test/dom.mjs` Install the DOM before importing Marionette and Lit. Both can capture their DOM environment during module initialization. ```js import { JSDOM } from 'jsdom'; const dom = new JSDOM('', { url: 'https://example.test/', }); globalThis.window = dom.window; globalThis.document = dom.window.document; globalThis.HTMLElement = dom.window.HTMLElement; ``` ## `test/title-editor.mjs` This local interaction changes only the View's own model. Lit retains the input across renders, and `live` compares the binding with its current DOM value. ```js import { View } from 'marionette'; import { DataApi } from '@mnjs/data'; import LitDomApi from '@mnjs/adapters/dom/lit-html'; import { html } from 'lit-html'; import { live } from 'lit-html/directives/live.js'; export const TitleEditor = View.extend({ template: ({ title }) => html`

${title}

`, ui: { title: '.title' }, events: { 'input @ui.title': 'editTitle' }, modelEvents: { 'change:title': 'render' }, editTitle() { this.model.set('title', this.getUI('title')[0].value); }, }).setDataApi(DataApi).setDomApi(LitDomApi); ``` ## `test/summary-application.mjs` Readiness stays with the Application. The feature requests `/summary.json`, which returns an object with a string `title`. Tests replace `fetch` to hold, resolve, or reject this boundary without a network or arbitrary delay. ```js import { Application, View } from 'marionette'; import LitDomApi from '@mnjs/adapters/dom/lit-html'; import { html } from 'lit-html'; const SummaryView = View.extend({ template: ({ title }) => html`

${title}

`, }).setDomApi(LitDomApi); export const SummaryApplication = Application.extend({ async prepareStart(_options, { signal }) { const response = await fetch('/summary.json', { signal }); if (!response.ok) throw new Error('Could not load the summary.'); return response.json(); }, onStart(_app, _options, summary) { this.showView(new SummaryView({ model: summary })); }, }); ``` The test runner restores its `fetch` mock after each test. Keep this recipe's tests sequential because they share a DOM and a global fetch boundary. ## `test/app.test.mjs` Each test owns a mount and disposes the resources it creates. Assertions use public methods and DOM outcomes; they avoid listener tables, internal flags, and lifecycle implementation details. The replacement test spies on the public `render` method to detect a removed View reacting to a shared Model, even if rendering a destroyed View would make no visible change. ```js import assert from 'node:assert/strict'; import { test } from 'node:test'; import { Region, View } from 'marionette'; import { Model } from '@mnjs/data'; import { TitleEditor } from './title-editor.mjs'; import { SummaryApplication } from './summary-application.mjs'; test('editing updates the model and rendered title', t => { const mount = document.body.appendChild(document.createElement('section')); const region = new Region({ el: mount }); const model = new Model({ title: 'Untitled' }); t.after(() => { region.destroy(); model.destroy(); mount.remove(); }); const editor = new TitleEditor({ model }); region.show(editor); const input = editor.getUI('title')[0]; input.value = 'Edited'; input.dispatchEvent(new window.Event('input', { bubbles: true })); assert.equal(model.get('title'), 'Edited'); assert.equal(editor.el.querySelector('.preview').textContent, 'Edited'); assert.equal(editor.getUI('title')[0], input); model.set('title', 'External change'); assert.equal(input.value, 'External change'); }); test('replacing a Region destroys the old View and releases its subscriptions', t => { const renders = t.mock.method(TitleEditor.prototype, 'render'); const mount = document.body.appendChild(document.createElement('section')); const region = new Region({ el: mount }); const model = new Model({ title: 'Original' }); t.after(() => { region.destroy(); model.destroy(); mount.remove(); }); const editor = new TitleEditor({ model }); region.show(editor); const input = editor.getUI('title')[0]; const oldRoot = editor.el; const replacement = new View({ template: () => '

Replacement

' }); region.show(replacement); assert.equal(editor.isDestroyed(), true); assert.equal(oldRoot.isConnected, false); assert.equal(region.currentView, replacement); assert.equal(model.isDestroyed(), false); const renderCount = renders.mock.callCount(); model.set('title', 'Still borrowed'); assert.equal(renders.mock.callCount(), renderCount); assert.equal(oldRoot.querySelector('.preview').textContent, 'Original'); input.value = 'Removed control'; input.dispatchEvent(new window.Event('input', { bubbles: true })); assert.equal(model.get('title'), 'Still borrowed'); assert.equal(mount.textContent, 'Replacement'); }); test('readiness precedes activation and stop destroys the screen', async t => { const mount = document.body.appendChild(document.createElement('section')); const request = Promise.withResolvers(); t.mock.method(globalThis, 'fetch', () => request.promise); const app = new SummaryApplication({ region: { el: mount } }); t.after(() => { app.destroy(); mount.remove(); }); const starting = app.start(); assert.equal(app.isRunning(), false); assert.equal(mount.childElementCount, 0); request.resolve(new Response(JSON.stringify({ title: 'Ready summary' }))); assert.equal(await starting, true); assert.equal(app.isRunning(), true); assert.equal(mount.querySelector('h1').textContent, 'Ready summary'); const screen = app.getView(); assert.equal(app.stop(), true); assert.equal(app.isRunning(), false); assert.equal(screen.isDestroyed(), true); assert.equal(mount.childElementCount, 0); const region = app.getRegion(); assert.equal(app.destroy(), true); assert.equal(app.isDestroyed(), true); assert.equal(region.isDestroyed(), true); }); test('failed readiness leaves an initially stopped feature without a screen', async t => { const mount = document.body.appendChild(document.createElement('section')); const failure = new Error('Unavailable'); t.mock.method(globalThis, 'fetch', async () => { throw failure; }); const app = new SummaryApplication({ region: { el: mount } }); t.after(() => { app.destroy(); mount.remove(); }); await assert.rejects(app.start(), error => error === failure); assert.equal(app.isRunning(), false); assert.equal(app.getView(), undefined); assert.equal(mount.childElementCount, 0); }); test('stop cancels pending readiness and an obsolete result cannot show UI', async t => { const mount = document.body.appendChild(document.createElement('section')); const request = Promise.withResolvers(); let preparationSignal; t.mock.method(globalThis, 'fetch', (_url, { signal }) => { preparationSignal = signal; return request.promise; }); const app = new SummaryApplication({ region: { el: mount } }); t.after(() => { app.destroy(); mount.remove(); }); const starting = app.start(); app.stop(); assert.equal(preparationSignal.aborted, true); request.resolve(new Response(JSON.stringify({ title: 'Obsolete summary' }))); assert.equal(await starting, false); assert.equal(app.isRunning(), false); assert.equal(mount.childElementCount, 0); }); ``` The render spy checks that a disposed editor no longer reacts to its borrowed Model. Unchanged DOM alone could miss a leaked callback because a destroyed View ignores rendering. For a View that updates through another public handler, observe that handler or its external effect instead. Run from the application root with the DOM preload: ```sh node --import ./test/dom.mjs --test --test-reporter=tap ./test/app.test.mjs ``` These tests do not need an HTTP server, timers, or a complete application. The pending fetch mock deliberately ignores cancellation so the final test verifies that a superseded startup cannot activate the feature. Failed preparation is handled by the caller through the rejected `start()` Promise. Tests for an application's chosen error presentation belong alongside that application's UI. ## Add browser checks where they matter JSDOM verifies DOM changes, delegated events, public lifecycle outcomes, and cleanup. It cannot establish real layout, focus and caret behavior while typing, browser navigation, or accessibility quality. Add browser tests for those user interactions and run them against your built consumer bundle. Test the behavior your application promises, including the chosen renderer and data provider. In your application, import your application setup first, then the real View and Application modules and test their promised public behavior with these patterns. See [local editing](/docs/guides/local-editing.md), [retained restart](/docs/guides/retained-restart.md), and [tooling](/docs/tooling.md) for related contracts and checks. [Canonical source](/docs/markdown/docs/guides/testing.md) · [Source identity](/docs/manifest.json) --- Document: docs/guides/typescript.md Canonical URL: https://marionettejs.com/docs/guides/typescript/ Markdown URL: https://marionettejs.com/docs/guides/typescript.md Reading SHA-256: 78766b415b2b0bc2da727ca75f0a3659d5fecef6ed6b25e55f398525cd0ac359 # Practical TypeScript Marionette and the companion packages ship declarations with their public entrypoints. Use strict TypeScript checking and the DOM library for browser code. Your bundler's module-resolution settings still apply; see [consumer tooling](/docs/tooling.md) for a checked project setup. ## Constructors, instances, options, and state `View` is a constructor value. A constructor created with `View.extend` keeps the options declared by its `initialize` method, custom methods, and the state returned by `createState`. Use `typeof` for the constructor and `InstanceType` for its instance. Broader public types such as `ViewInstance` are useful at boundaries that only need core methods; they do not describe your custom methods. ```ts import { View } from 'marionette'; type CounterOptions = { label: string }; const CounterView = View.extend({ tagName: 'button', template: false, initialize(options: CounterOptions) { this.el.textContent = options.label; }, createState() { return { count: 0 }; }, events: { click: 'increment' }, increment() { this.getState().count++; }, label() { return this.options.label; } }); type CounterConstructor = typeof CounterView; type Counter = InstanceType; const counter: Counter = new CounterView({ label: 'Count' }); const label: string = counter.label(); const count: number = counter.getState().count; ``` Here `new CounterView()` and a numeric `label` are type errors. A typed `initialize` signature can declare the constructor options even when its body needs no setup, as in the later examples. Custom options remain in `this.options`; Marionette does not copy every option onto the instance. Read them there or with `getOption`. Each `createState` call creates this View's own state. Constructor-supplied `state` is borrowed and replaces the factory state; see [state ownership](/docs/api/shared/state.md). A native `class` can extend `CounterView` and inherit these types. Native class fields run after `super`, including Marionette initialization: avoid using them to replace configuration needed during construction or values assigned by `initialize`. Preserve parent lifecycle behavior when overriding methods. The [View reference](/docs/api/view.md#typescript) lists the public instance and constructor types. ## Local DOM interactions DOM event targets and UI matches need runtime narrowing. With the native delegator, `delegateTarget` is the element matched by the selector; `target` can be a nested element. This example uses the native delegator; a custom delegator must provide the matched-element behavior this handler relies on. `getUI` can return `undefined`, and its default public query type is `ArrayLike`, so index it rather than assuming NodeList or jQuery methods. This local editor owns a plain state object. It uses a class-scoped Lit adapter to insert text safely and a delegated DOM handler to update state. Lit updates the preview while retaining the input and its caret. ```ts import { View, type DelegatedEvent } from 'marionette'; import LitDomApi from '@mnjs/adapters/dom/lit-html'; import { html } from 'lit-html'; import { live } from 'lit-html/directives/live.js'; const EditorView = View.extend({ template: ({ title }: { title: string }) => html`

${title}

`, templateContext() { return this.getState(); }, ui: { title: '.title' }, createState() { return { title: 'Untitled' }; }, events: { 'input @ui.title': 'editTitle' }, editTitle(event: DelegatedEvent) { const input = event.delegateTarget; if (!(input instanceof HTMLInputElement)) { return; } this.getState().title = input.value; this.render(); }, focusTitle() { const input = this.getUI('title')?.[0]; if (input instanceof HTMLInputElement) { input.focus(); } } }); EditorView.setDomApi(LitDomApi); const editor = new EditorView().render(); document.body.append(editor.el); editor.focusTitle(); ``` The element checks establish what the code uses: `value` and `focus`. They also handle missing UI bindings. These types do not guarantee the selector exists in every template. Event maps use string names; TypeScript does not verify that a method annotation matches every event payload. Interaction tests verify that delivery. No Application is needed for this View's local interaction. Use [local editing](/docs/guides/local-editing.md) when a borrowed Model supplies the data. ## Readiness results and lifecycle completion An Application's `prepareStart` returns data required before activation. `onStart` receives that resolved result as its third argument. The public `start` and `restart` operations return `Promise`; `stop` and `destroy` return synchronous booleans. The boolean reports lifecycle completion rather than preparation data. Handle rejected start/restart Promises and synchronous teardown errors at the calling boundary. TypeScript does not validate a server response. Treat parsed JSON as `unknown`, validate the fields your feature needs, and only then return a typed result. This example validates one title field; use your application's API schema validation for larger responses. ```ts import { Application, View, type LifecycleContext } from 'marionette'; import LitDomApi from '@mnjs/adapters/dom/lit-html'; import { html } from 'lit-html'; type Summary = { title: string }; const SummaryView = View.extend({ initialize(_options: Summary) {}, templateContext() { return { title: this.options.title }; }, template: ({ title }: Summary) => html`

${title}

` }); SummaryView.setDomApi(LitDomApi); const SummaryApplication = Application.extend({ async prepareStart(_options: unknown, { signal }: LifecycleContext): Promise { const response = await fetch('/summary.json', { signal }); if (!response.ok) { throw new Error('Could not load summary'); } const value: unknown = await response.json(); if (typeof value !== 'object' || value === null || !('title' in value) || typeof value.title !== 'string') { throw new Error('Invalid summary response'); } return { title: value.title }; }, onStart(_application: unknown, _options: unknown, summary: Summary) { this.setView(new SummaryView(summary)); this.showView(); } }); const mount = document.createElement('main'); document.body.append(mount); const app = new SummaryApplication({ region: { el: mount } }); const activation: Promise = app.start(); const activated: boolean = await activation; ``` The Application owns readiness and the displayed View. Pass the lifecycle signal to transport; stopping or destroying during pending preparation prevents that startup from activating and aborts its signal. An active restart prepares new data while retaining the current UI; commit its result in `onStart` and construct the shell once. See [retained restart](/docs/guides/retained-restart.md). Reuse the same `Summary` type in preparation and the explicitly annotated completion hook. Keep concrete application types when hooks or subclasses rely on an inherited preparation-result contract; the general `ApplicationInstance` type defaults that result to `unknown`. ## Typed data and provider boundaries Marionette's `model` and `collection` properties are `unknown` at the general View boundary because data providers are replaceable. Registering `DataApi` does not narrow those properties. For a feature using native Models, declare the source in initializer options and access the typed option directly. The option and the View's `model` refer to the same borrowed source in this example. ```ts import { View } from 'marionette'; import { Model, DataApi } from '@mnjs/data'; import LitDomApi from '@mnjs/adapters/dom/lit-html'; import { html } from 'lit-html'; type TitleAttributes = { title: string }; type TitleOptions = { model: Model }; const TitleView = View.extend({ initialize(_options: TitleOptions) {}, templateContext() { return { title: this.options.model.get('title') ?? 'Untitled' }; }, template: ({ title }: { title: string }) => html`

${title}

`, modelEvents: { 'change:title': 'render' } }); TitleView.setDataApi(DataApi); TitleView.setDomApi(LitDomApi); const model = new Model({ title: 'Overview' }); const title = new TitleView({ model }).render(); const current: string | undefined = model.get('title'); ``` `Model.get` can return `undefined` even with typed attributes: Models accept partial attributes and fields can be removed. A View borrows its Model; destroying the View releases its subscription without destroying that source. `@mnjs/data` provides local Models, Collections, and adapters, with no native `fetch`, `save`, or server synchronization. It may sit behind an API layer or be replaced with another data solution. Define and validate types at that solution's boundary; choose a compatible [data provider](/docs/api/providers/data.md) for Marionette. Type annotations establish shape, while providers and ownership establish runtime behavior. [Canonical source](/docs/markdown/docs/guides/typescript.md) · [Source identity](/docs/manifest.json) --- Document: docs/guides/local-editing.md Canonical URL: https://marionettejs.com/docs/guides/local-editing/ Markdown URL: https://marionettejs.com/docs/guides/local-editing.md Reading SHA-256: 1d83547675edbd11dc634b8f2405c99543e5e01f199e84cadea1a4e8c39ff5c7 # Edit a View's own model Keep an interaction local when it changes the data displayed by that View and needs no broader workflow. This example edits a title as the user types and also reflects changes made elsewhere to the same Model. Start with the [renderer and data setup](/docs/integrations/setup.md). `@mnjs/data` is optional and supplies observable attributes here. See that setup page for API access and persistence. Add a mount to your page: ```html
``` In the entry module: ```js import { Region, View, setDataApi, setDomApi } from 'marionette'; import { DataApi, Model } from '@mnjs/data'; import LitDomApi from '@mnjs/adapters/dom/lit-html'; import { html } from 'lit-html'; import { live } from 'lit-html/directives/live.js'; setDataApi(DataApi); setDomApi(LitDomApi); const TitleEditor = View.extend({ template: ({ title }) => html`

${title}

`, ui: { title: '.title', preview: '.preview' }, events: { 'input @ui.title': 'editTitle' }, modelEvents: { 'change:title': 'render' }, editTitle() { this.model.set('title', this.getUI('title')[0].value); } }); const model = new Model({ title: 'Untitled' }); const region = new Region({ el: '#editor' }); const editor = new TitleEditor({ model }); region.show(editor); ``` The input event changes the Model, whose observable change renders the View with Lit. Lit updates the existing nodes. Its `live` binding compares against the input's current DOM value, so typing keeps focus and selection while an external Model update changes the same input. Lit owns both the input binding and preview content; do not replace its interpolated content manually. This is immediate local editing, with no separate draft or Save operation. If the selected data solution supplies a persistence method, a View can save its own model as part of a local interaction. `@mnjs/data` has no `save()` method; integrate an API layer or choose another data solution for persistence. Coordinate shared decisions, navigation, or readiness in an owner with the corresponding lifetime; see [architecture](/docs/architecture.md). When the panel closes, `region.empty()` destroys the editor and removes its model subscriptions. The supplied Model remains alive: the code that created or shared it decides when it should be destroyed. Destroy the Region when its owner finishes. ## Keep a draft until Save succeeds For an explicit Save interaction, keep input in View-owned state and update the supplied Model only after the API accepts it. The draft belongs to this editor's lifetime, so the View can own the request too. This independent example assumes `PATCH /api/titles/:id` accepts JSON `{ title }` and returns JSON `{ title }` containing the accepted value. The API function supplies persistence; optional `@mnjs/data` supplies observable attributes and has no `save()` method. ```js import { Region, View, setDataApi, setDomApi, setStateApi } from 'marionette'; import { DataApi, Model, StateApi } from '@mnjs/data'; import LitDomApi from '@mnjs/adapters/dom/lit-html'; import { html } from 'lit-html'; import { live } from 'lit-html/directives/live.js'; setDataApi(DataApi); setDomApi(LitDomApi); setStateApi(StateApi); async function saveTitle(id, title, signal) { const response = await fetch(`/api/titles/${encodeURIComponent(id)}`, { method: 'PATCH', signal, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title }) }); if (!response.ok) throw new Error('Save failed'); return (await response.json()).title; } const DraftEditor = View.extend({ createState() { return new Model({ draftTitle: this.model.get('title'), saving: false, status: '', error: '' }); }, stateEvents: { change: 'render' }, templateContext() { return this.getState().toObject(); }, template: ({ draftTitle, saving, status, error }) => html`

${status}

${error}

`, ui: { title: '.title' }, events: { 'input @ui.title': 'editDraft', 'submit form': 'save' }, editDraft() { const state = this.getState(); if (state.get('saving')) return; state.set({ draftTitle: this.getUI('title')[0].value, status: '' }); }, async save(event) { event?.preventDefault(); const state = this.getState(); if (this.isDestroyed() || state.get('saving')) return false; const request = new AbortController(); this.saveRequest = request; state.set({ saving: true, status: 'Saving…', error: '' }); let title; try { title = await saveTitle(this.model.get('id'), state.get('draftTitle'), request.signal); } catch { if (this.saveRequest !== request || this.isDestroyed()) return false; this.saveRequest = undefined; state.set({ saving: false, status: '', error: 'Could not save. Your draft is still here; try again.' }); return false; } if (this.saveRequest !== request || this.isDestroyed()) return false; this.saveRequest = undefined; this.model.set('title', title); state.set({ draftTitle: title, saving: false, status: 'Saved.' }); return true; }, onBeforeDestroy() { const request = this.saveRequest; this.saveRequest = undefined; request?.abort(); } }); const mount = document.createElement('section'); document.body.append(mount); const savedTitle = new Model({ id: 1, title: 'Original' }); const draftRegion = new Region({ el: mount }); const draftEditor = new DraftEditor({ model: savedTitle }); draftRegion.show(draftEditor); ``` Typing changes only the draft. During Save the input is read-only, and repeated submissions are ignored. The button stays focusable, with `aria-disabled` announcing its pending state. Failure preserves the draft, input nodes, and supplied Model so the user can retry. Success commits the API's accepted title and displays it in the input. External Model changes do not overwrite an open draft; this example starts a new draft when a new editor is created. `draftRegion.empty()` destroys the editor and its owned state while leaving `savedTitle` alive. Every asynchronous completion checks that it still belongs to the live editor, including transports that ignore abort. Cancellation prevents a late result from changing local state; it does not guarantee that the server undoes a save. Destroy the Region and remove the mount when their owner finishes. ## Check the behavior Type into the input and verify the preview updates without losing focus. Update the Model externally and verify both elements change. Empty the Region, change the Model again, and verify the removed editor no longer reacts. See [tooling](/docs/tooling.md) for package checks and debugging. For the draft example, type and verify `savedTitle` stays unchanged until Save succeeds. Fail a request, then retry the same draft. Close the editor during Save and verify a late response cannot update the supplied Model or detached input. [Canonical source](/docs/markdown/docs/guides/local-editing.md) · [Source identity](/docs/manifest.json) --- Document: docs/guides/retained-restart.md Canonical URL: https://marionettejs.com/docs/guides/retained-restart/ Markdown URL: https://marionettejs.com/docs/guides/retained-restart.md Reading SHA-256: 550cac341f34c8275d9c9692c12391b6e83386e6d2efc8556259be8eac3bfca1 # Restart readiness while retaining UI Use `Application.restart()` to load readiness again while keeping the feature active. Pending preparation retains the root, child Applications, subscriptions, and state event delivery. Successful `onStart` code decides what changes on screen. This dashboard reloads a summary while preserving its page and unfinished notes. `GET /summary.json` returns `{ "openTasks": number }`. API transport is separate from the optional `@mnjs/data` observable state. Install the packages in [setup](/docs/integrations/setup.md). ## Construct the shell once ```js import { Application, View, setDataApi, setDomApi } from 'marionette'; import { Model, DataApi, StateApi } from '@mnjs/data'; import LitDomApi from '@mnjs/adapters/dom/lit-html'; import { html } from 'lit-html'; setDataApi(DataApi); setDomApi(LitDomApi); async function loadSummary(signal) { const response = await fetch('/summary.json', { signal }); if (!response.ok) throw new Error('Could not load the summary.'); return response.json(); } const PageView = View.extend({ template: () => html`
`, regions: { summary: '.summary', notes: '.notes' }, triggers: { 'click .reload': 'reload' }, }); const SummaryView = View.extend({ template: ({ openTasks, loading, error }) => html`

Open tasks: ${openTasks}

${loading ? 'Loading…' : error}

`, modelEvents: { change: 'render' }, }); const NotesView = View.extend({ template: () => html``, }); const DashboardApplication = Application.extend({ createState() { return new Model({ openTasks: 0, loading: false, error: '' }); }, viewEvents: { reload: 'reloadSummary' }, onBeforeStart() { this.getState().set({ loading: true, error: '' }); }, prepareStart(_options, { signal }) { return loadSummary(signal); }, onStart(_app, _options, summary) { const state = this.getState(); state.set({ openTasks: summary.openTasks, loading: false, error: '' }); if (this.getView()) return; const page = this.setView(new PageView()); page.showChildView('summary', new SummaryView({ model: state })); page.showChildView('notes', new NotesView()); this.showView(); }, reloadSummary() { return this.restart().catch(error => { this.getState().set({ loading: false, error: error.message }); }); }, }).setStateApi(StateApi); const mount = document.createElement('main'); document.body.append(mount); const app = new DashboardApplication({ region: { el: mount } }); await app.start(); ``` Application owns the Model created by `createState`; SummaryView borrows it. Each successful result updates that Model. Only SummaryView renders again. PageView, NotesView, and the textarea retain their identity and unfinished input. `viewEvents` connects the page's intent once; restart does not install another listener. `reloadSummary` is the button's error boundary around `restart()`. It adds no request controller or concurrency layer. Initial `start()` failure rejects to its caller before the page exists. A failed reload leaves the running page and previous summary available and shows the error in its status area. ## Let preparation discard obsolete results Each restart supersedes pending preparation, aborts its signal, and resolves the previous call `false`. Only the current successful result reaches `onStart`, even when a transport ignores abort. Applying data there prevents obsolete requests from changing the summary. The current failure rejects; the button's handler reports it. A cancelled call never reaches that catch handler. Pass the preparation signal to the transport. After preparation completes, that signal is no longer a lifetime signal. Independent operations such as saving a draft need their own cancellation policy; they do not belong in readiness merely to acquire one. ## End the run deliberately `stop()` synchronously cancels preparation and destroys the page and its child Views. A later `start()` loads again and builds a new page. `destroy()` also disposes registered children and owned state. An [external host](/docs/guides/existing-ui.md) must stop or destroy its feature when removing its mount; removing DOM alone does not end Application authority. Restart itself does not render or replace Views. Calling `setView(new PageView())` unconditionally in `onStart`, or rendering the existing PageView again, would replace its child UI and lose the notes. Preserve the shell and update the source or Region that changed. Use `app.stop(); await app.start(options)` when full reconstruction or a new host is intended. The notes here are local input. If they must survive stopping or leaving the page, give the draft a longer-lived owner. [Application lifecycle](/docs/api/application.md) · [State ownership](/docs/api/shared/state.md) · [View rendering](/docs/api/view.md#rendering-and-status) [Canonical source](/docs/markdown/docs/guides/retained-restart.md) · [Source identity](/docs/manifest.json) --- Document: docs/guides/existing-html.md Canonical URL: https://marionettejs.com/docs/guides/existing-html/ Markdown URL: https://marionettejs.com/docs/guides/existing-html.md Reading SHA-256: 51e7d0d723d3c2478ddad5ea7fbbcbbac9ffe98a712ee8351305186b4bb71a60 # Add behavior to existing HTML A View can adopt server-rendered or static markup. Set `el` to the existing root and `template: false` to retain its contents. No renderer or data package is needed for this example. Start with this HTML: ```html
``` Run this module through your bundler after the markup is available. A native browser module script needs an import map to resolve the bare package import: ```js import { View } from 'marionette'; const DetailsView = View.extend({ template: false, ui: { toggle: 'button', body: '#details-body' }, events: { 'click @ui.toggle': 'toggleDetails' }, toggleDetails() { const body = this.getUI('body')[0]; body.hidden = !body.hidden; this.getUI('toggle')[0].setAttribute('aria-expanded', String(!body.hidden)); } }); const details = new DetailsView({ el: document.querySelector('#details') }); ``` `el` takes the actual DOM element. Construction binds the existing controls without replacing them. Because this root already has content and is in the document, `details.isRendered()` and `details.isAttached()` are true immediately. Construction does not emit render or attach events: perform any initial enhancement directly in `initialize`, rather than waiting for `onRender` or `onAttach`. There is no `render()` call here. With `template: false`, calling it later also preserves the markup and emits no render event. The View can still own [named Regions](/docs/api/view.md#named-regions) and display managed children inside them without rendering the parent. ## Own the adopted element Adoption gives the View responsibility for its root. `details.destroy()` removes the adopted section, cleans up its event bindings, and destroys any managed child Views. It does not leave the original markup behind. Use a root whose lifetime matches that responsibility. This small example creates the View directly, so the code managing the page must destroy it when that page ends. For a larger feature, assign it to the appropriate Region or Application owner. An already attached View does not receive a new attach notification simply because a Region adopts it. See [Region ownership](/docs/api/region.md#showing-a-view) when moving existing Views between owners. ## Check the behavior Verify the button is the same DOM node before and after construction, and that clicking toggles both visibility and `aria-expanded`. Calling `render()` should preserve those nodes and their current state. Destroying the View should remove the section and end its interaction handlers. [Canonical source](/docs/markdown/docs/guides/existing-html.md) · [Source identity](/docs/manifest.json) --- Document: docs/guides/existing-ui.md Canonical URL: https://marionettejs.com/docs/guides/existing-ui/ Markdown URL: https://marionettejs.com/docs/guides/existing-ui.md Reading SHA-256: 597ae8a995f846d4cec2911d7d763a1ab73a3ff2169dfaac57164a3732dc155e # Integrate with existing UI A page shell, custom element, or another UI framework can host a Marionette feature. Give the host responsibility for a stable mounting element and Marionette responsibility for the content inside it. The host must leave that content alone until the feature ends. ## Mount a View in a host-owned element This small feature needs local interaction and a lifetime, so a Region owns its View. No Application or data provider is required. Install the rendering packages from the [quick start](/docs/quick-start.md). ```js import { Region, View } from 'marionette'; import LitDomApi from '@mnjs/adapters/dom/lit-html'; import { html } from 'lit-html'; const CounterView = View.extend({ initialize() { this.count = 0; }, template: ({ count }) => html``, templateContext() { return { count: this.count }; }, events: { 'click button': 'increment' }, increment() { this.count++; this.render(); this.triggerMethod('count:changed', this.count); }, }).setDomApi(LitDomApi); export function mountCounter(el, onCountChanged) { const region = new Region({ el }); const counter = new CounterView(); region.listenTo(counter, { 'count:changed': onCountChanged }); region.show(counter); return { destroy() { region.destroy(); }, }; } ``` Call `mountCounter` when the host's empty element is available. The callback receives the new count; the feature keeps its local presentation state. Call the returned `destroy()` from the host's cleanup hook **before** removing or replacing that element. Region destruction destroys the View, releases subscriptions, and leaves the mount itself in place. Repeated cleanup is safe. Mounting again creates a new feature with fresh state. Use the host framework's mount/ref and cleanup hooks to perform these two operations. Keep the mounting element's identity stable across host updates. Give each mount its own Region; do not share a live View between Regions. If the host provides observable data, the View can borrow it through the configured [data provider](/docs/api/providers/data.md); destroying the View does not dispose borrowed data. ## Host a feature with asynchronous readiness Use an [Application](/docs/api/application.md) for feature readiness or coordinated child Applications. Give it `region: { el }`; the host owns its `start()` and `destroy()` calls and handles their failures. Follow [startup and cancellation](/docs/api/application.md#preparation-cancellation-and-failure) and the executable [pending-start teardown test](/docs/guides/testing.md) for the lifecycle contract. Keep the Application reference available to host cleanup while startup is pending. Call synchronous `app.destroy()` in the host cleanup hook before reusing the mount. It cancels pending preparation and releases the feature UI. Finish any required asynchronous business work before invoking cleanup, according to the host’s lifecycle. Removing DOM alone does not destroy a View or Application. Likewise, Marionette does not destroy the surrounding host. For markup whose root itself belongs to Marionette, see [adopting existing HTML](/docs/guides/existing-html.md); that View's destruction removes its root. ## Check the boundary Test a click and callback, cleanup with a retained button reference, and a fresh mount. Confirm cleanup preserves the host element and prevents detached controls from producing callbacks. For asynchronous integrations, extend the consumer cancellation test with your actual host cleanup hook. Use [consumer testing](/docs/guides/testing.md) for lifecycle checks and a browser test for the real host's mount/cleanup behavior. [Canonical source](/docs/markdown/docs/guides/existing-ui.md) · [Source identity](/docs/manifest.json) --- Document: docs/guides/routing.md Canonical URL: https://marionettejs.com/docs/guides/routing/ Markdown URL: https://marionettejs.com/docs/guides/routing.md Reading SHA-256: 764bfa2140e322e02b2f11f510d87fa831c8ee438dd0e4c51d73e3fe59f7aae4 # Connect navigation to feature ownership Marionette v5 supplies UI and Application lifecycles. Choose browser navigation or a separate router for URL matching and history. Connect route decisions to the owner that shows or starts the destination feature. ## Keep a shell while changing its content This small application uses fragment links for two synchronous pages. Its Application owns the shell and the browser subscription; the content Region owns the current page. No data provider is needed. The rendering packages are described in [setup](/docs/integrations/setup.md). ```js import { Application, View } from 'marionette'; import LitDomApi from '@mnjs/adapters/dom/lit-html'; import { html } from 'lit-html'; const PageView = View.extend({ template: () => html`
`, regions: { content: '.content' }, }).setDomApi(LitDomApi); const HomeView = View.extend({ template: () => html`

Home

Welcome.

`, }).setDomApi(LitDomApi); const AboutView = View.extend({ template: () => html`

About

About this application.

`, }).setDomApi(LitDomApi); const NotFoundView = View.extend({ template: () => html`

Page not found

`, }).setDomApi(LitDomApi); const NavigationApplication = Application.extend({ onStart() { if (!this.getView()) { this.showView(new PageView()); this.onHashChange = () => this.showRoute(); window.addEventListener('hashchange', this.onHashChange); } this.showRoute({ focus: false }); }, showRoute({ focus = true } = {}) { const route = window.location.hash || '#home'; if (route === this.currentRoute) return; this.currentRoute = route; const Destination = route === '#home' ? HomeView : route === '#about' ? AboutView : NotFoundView; const destination = new Destination(); this.getView().showChildView('content', destination); const heading = destination.el.querySelector('h1'); document.title = heading.textContent; if (focus) heading.focus(); }, onStop() { window.removeEventListener('hashchange', this.onHashChange); this.currentRoute = undefined; }, }); const mount = document.createElement('div'); document.body.append(mount); const app = new NavigationApplication({ region: { el: mount } }); await app.start(); ``` Startup reads the current fragment explicitly, so a direct link to `#about` works. Following a link changes the URL; `hashchange` selects the destination. Browser Back/Forward uses the same path. The shell and its navigation remain mounted while Region replacement destroys the previous page. Repeated dispatch of the same route keeps the current page. Unknown fragments show a separate not-found View. Each destination updates the document title. Later navigation moves focus to its heading; startup leaves focus alone. Decide the focus policy for your actual navigation, especially when a route update merely filters retained content. A route parameter change need not replace the whole feature: update its owned state or use the destination Application’s [retained restart](/docs/guides/retained-restart.md) when appropriate. Stopping destroys the shell and its children and removes the Window listener. Starting again reads the current URL and creates a fresh shell. Native `addEventListener` subscriptions need this explicit cleanup; Marionette's `listenTo` cleanup handles Marionette event sources, not Window subscriptions. Restart retains the shell and subscription; this example installs them only when no root exists. Route changes update the content Region directly. ## Choose the URL owner Fragment navigation is useful for a small standalone application. If an existing host already owns routing, let it select the feature and follow the [existing UI boundary](/docs/guides/existing-ui.md). Avoid starting a second global URL listener for an embedded feature. For path-based routing, configure the server to serve the application entry for valid direct links and reloads. A History API implementation must handle `popstate` for traversal and explicitly dispatch its own `pushState`/`replaceState` transitions: those calls do not emit `popstate`, and changing a fragment through them does not emit `hashchange`. See the browser contracts for [pushState](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState) and [hashchange](https://developer.mozilla.org/en-US/docs/Web/API/Window/hashchange_event). Choose a routing library when matching, nested routes, redirects, or transition policies warrant it; Marionette ownership remains the same. ## Route to asynchronous features When a destination needs readiness, let the navigation Application manage a child Application in the content Region. This independent example expects `GET /pages/home.json` and `GET /pages/about.json` to return `{ "title": string, "body": string }`. It keeps the shell while stopping the previous child run, then replaces the content with loading, the ready page, or an error. These Views share one Region, so only one is visible. ```js import { Application, View } from 'marionette'; import LitDomApi from '@mnjs/adapters/dom/lit-html'; import { html } from 'lit-html'; const ShellView = View.extend({ template: () => html`
`, regions: { content: '.content' }, }).setDomApi(LitDomApi); const MessageView = View.extend({ template: ({ title, body }) => html`

${title}

${body}

`, }).setDomApi(LitDomApi); const ContentView = View.extend({ template: ({ title, body }) => html`

${title}

${body}

`, }).setDomApi(LitDomApi); const ErrorView = View.extend({ template: ({ message }) => html`

Could not load page

${message}

`, triggers: { 'click .retry': 'retry' }, }).setDomApi(LitDomApi); const PageApplication = Application.extend({ async prepareStart({ route }, { signal }) { const response = await fetch(`/pages/${route.slice(1)}.json`, { signal }); if (!response.ok) throw new Error('The page request failed.'); return response.json(); }, onStart(_app, _options, page) { this.showView(new ContentView({ model: page })); document.title = page.title; }, }); const NavigationApplication = Application.extend({ childApps: { page: PageApplication }, onStart() { if (this.getView()) return; this.showView(new ShellView()); this.onHashChange = () => { this.navigationTask = this.showRoute(); this.navigationTask.catch(error => console.error(error)); }; window.addEventListener('hashchange', this.onHashChange); this.onHashChange(); }, async showRoute() { if (!this.isRunning()) return false; const route = window.location.hash || '#home'; if (route === this.currentRoute) return false; this.currentRoute = route; const child = this.getChildApp('page'); child.stop(); const region = this.getView().getRegion('content'); if (route !== '#home' && route !== '#about') { document.title = 'Page not found'; region.show(new MessageView({ model: { title: 'Page not found', body: 'Choose Home or About.', } })); return true; } document.title = 'Loading page…'; region.show(new MessageView({ model: { title: 'Loading page…', body: 'Please wait.', } })); try { return await child.start({ region, route }); } catch (error) { child.stop(); this.currentRoute = undefined; document.title = 'Could not load page'; const errorView = new ErrorView({ model: { message: error.message } }); this.listenTo(errorView, { retry: this.onHashChange }); region.show(errorView); return false; } }, onBeforeStop() { this.currentRoute = undefined; window.removeEventListener('hashchange', this.onHashChange); }, }); const mount = document.createElement('div'); document.body.append(mount); const app = new NavigationApplication({ region: { el: mount } }); await app.start(); ``` The URL selects the destination as soon as it changes. A failed request keeps that URL and shows an error View; Retry dispatches the current URL again. An unknown fragment shows a not-found View. Repeating a route that is loading or ready leaves it alone. The owner stops the outgoing child before starting the next destination. Stopping the child cancels its pending start, which resolves `false`; obsolete failures do not reject that cancelled call. The next destination starts a new preparation. Stopping the outgoing child aborts its preparation signal; an obsolete result cannot activate even if the transport ignores abort. A rejected start is stopped before error presentation replaces its UI. `await app.start()` activates the shell; `navigationTask` tracks the separately started page transition. The child loads readiness data in `prepareStart` and renders it in `onStart`. Stop is unconditional here: `onBeforeStop` removes the Window subscription before child teardown. Stop destroys the shell; a later start creates a new one and reads the current URL. Restart retains the shell, the Window listener, and the current child run. Destroy also disposes the registered child. See [Application composition](/docs/api/application.md#child-applications) and [consumer testing](/docs/guides/testing.md). ## Check navigation Test direct links, link clicks, Back/Forward, unknown routes, and repeat selection. Verify shell identity survives page changes, outgoing pages are destroyed, and stop/start removes and reinstalls one URL subscription; restart retains that single subscription. Use a real browser for history and focus behavior. [Canonical source](/docs/markdown/docs/guides/routing.md) · [Source identity](/docs/manifest.json) --- Document: docs/guides/lists.md Canonical URL: https://marionettejs.com/docs/guides/lists/ Markdown URL: https://marionettejs.com/docs/guides/lists.md Reading SHA-256: 148524293e75ad81f3b12a2669df16cd0c970741ffee72f21d06ce5db360dfd8 # Show, sort, and filter a list Use CollectionView for repeated children that share one placement rule. A surrounding View can own the list's local controls and Region. Add an Application when the feature needs API readiness or broader coordination; sorting a displayed list does not require one. This catalog uses the optional `@mnjs/data` Collection for observable membership. Fetching and persistence need an API layer or another data provider. Install the rendering and data packages from [setup](/docs/integrations/setup.md). ## Own the controls and repeated rows ```js import { CollectionView, Region, View } from 'marionette'; import { Collection, DataApi } from '@mnjs/data'; import LitDomApi from '@mnjs/adapters/dom/lit-html'; import { html } from 'lit-html'; const ItemView = View.extend({ tagName: 'li', template: ({ label }) => html` ${label} `, modelEvents: { change: 'render' }, }).setDataApi(DataApi).setDomApi(LitDomApi); const EmptyView = View.extend({ tagName: 'li', template: () => html`No matching items.`, }).setDomApi(LitDomApi); const ItemList = CollectionView.extend({ tagName: 'ul', childView: ItemView, emptyView: EmptyView, viewComparator: 'label', }).setDataApi(DataApi); const CatalogView = View.extend({ template: () => html`
`, regions: { items: '.items' }, ui: { available: '.available' }, events: { 'change @ui.available': 'filterItems', 'click .reverse': 'reverseItems' }, onRender() { this.descending = false; this.getUI('available')[0].checked = false; this.list = new ItemList({ collection: this.options.items }); this.showChildView('items', this.list); }, filterItems() { this.list.setFilter(this.getUI('available')[0].checked ? { available: true } : null); }, reverseItems() { this.descending = !this.descending; this.list.setComparator(this.descending ? (left, right) => right.model.get('label').localeCompare(left.model.get('label')) : 'label'); }, }).setDomApi(LitDomApi); const items = new Collection([ { id: 'a', label: 'Apricot', available: true }, { id: 'b', label: 'Blueberry', available: false }, ]); const catalog = new CatalogView({ items }); const mount = document.createElement('section'); document.body.append(mount); const region = new Region({ el: mount }); region.show(catalog); ``` The outer Region owns CatalogView; its named Region owns ItemList; the list owns rows and its empty View. The collection is borrowed. Destroy the outer Region when this feature ends, then dispose data according to the lifetime of the code that created it. The notes fields are temporary DOM input. Sorting moves the existing rows; filtering detaches excluded rows and retains their Views and input values. Returning a row preserves its nodes, but leaving the document can lose focus. When a filter hides the focused row, choose a focus destination such as the filter control. See [accessibility and rendering](/docs/guides/accessibility-rendering.md). ## Update without rebuilding everything | Change | Operation | Effect | | --- | --- | --- | | Add or remove a member | `items.add(...)` / `items.remove(...)` | Creates/destroys affected rows and retains surviving rows. Removing membership does not destroy the Model. | | Change a displayed attribute | `model.set(...)` | This example's `modelEvents` renders that row. | | Change an attribute used by sorting/filtering | Then call `catalog.list.sort()` / `filter()` | Recomputes presentation; an `@mnjs/data` Model change does not automatically rerun the parent criteria. | | Show all rows | `catalog.list.removeFilter()` | Restores retained hidden rows. | | Replace the membership snapshot | `items.reset(...)` | Destroys and rebuilds rows, including rows for retained Models. | For a group of additions or removals, use `items.add([...])` or `items.remove([...])` rather than calling the method in a loop. When membership changes, the `@mnjs/data` Collection emits one aggregate update per call, so CollectionView reconciles once per call instead of once per item while retaining surviving rows. Use `reset` for a whole replacement snapshot; its row destruction makes it unsuitable for preserving input or focus. Avoid full `list.render()` to apply a filter or sort: it rebuilds managed rows. Rendering CatalogView again also destroys its Region children; this example deliberately resets its local controls when rebuilding the list. If notes must survive those operations or navigation, put the draft in an appropriately owned data source. An empty presentation can mean all rows are filtered out even when the collection has members. `emptyView` describes that presentation, rather than storing an empty record in the collection. Give it a root valid for the container, such as `li` inside `ul`. ## Identity and scale The configured DataApi determines keys and collection observation. With `@mnjs/data`, the key is Model `cid`; reusing a server `id` in a new Model does not reuse the old row. Other providers must supply stable unique keys, but a replacement model object still replaces its View. `list.children` describes the currently presented rows, excluding hidden rows and the empty View. Save a row reference before hiding it when checking identity. CollectionView is not a virtualized viewport: hidden rows remain alive. For large datasets, choose paging or a measured windowing solution based on the application's needs. ## Check the behavior Enter notes, sort, hide and restore that row, and check the same input and value return. Add/remove members and verify surviving row identity and outgoing destruction. With Available only selected, call `items.at(0).set('available', false)` and `catalog.list.filter()` to filter every row out; verify empty presentation without changing collection length. Finish by destroying the Region and checking that no borrowed data was destroyed or continues rendering the old list. [CollectionView reference](/docs/api/collection-view.md) defines the full contract; [consumer testing](/docs/guides/testing.md) gives a test setup. [Canonical source](/docs/markdown/docs/guides/lists.md) · [Source identity](/docs/manifest.json) --- Document: docs/guides/widgets.md Canonical URL: https://marionettejs.com/docs/guides/widgets/ Markdown URL: https://marionettejs.com/docs/guides/widgets.md Reading SHA-256: 8ad9d2fb584e8174de7ca8b9a23ab27adc12c8d8a94c685cef90e95ff8edbc6f # Integrate an imperative control Keep a control's DOM and cleanup with the View that owns its element. A one-off control can stay in that View. Use a [Behavior](/docs/api/behavior.md) when multiple Views genuinely share the same integration. This example uses the browser's native dialog API. It needs no renderer, data package, Application, or widget wrapper. Show the View through a Region before opening the dialog: [`showModal()` requires a connected element](https://html.spec.whatwg.org/multipage/interactive-elements.html#dom-dialog-showmodal). ```js import { Region, View } from 'marionette'; const HelpView = View.extend({ template: () => `

Help

Choose a record to see its details.

`, ui: { open: '.open-help', dialog: 'dialog' }, events: { 'click @ui.open': 'openDialog' }, openDialog() { this.getUI('dialog')[0].showModal(); }, closeDialog() { const dialog = this.el.querySelector('dialog'); if (dialog && dialog.open) dialog.close(); }, onDomRemove() { this.closeDialog(); }, onBeforeDestroy() { this.closeDialog(); }, }); const mount = document.body.appendChild(document.createElement('section')); const region = new Region({ el: mount }); const help = new HelpView(); region.show(help); ``` The native dialog owns modal display, focus handling, Escape dismissal, and its closing form. The View owns the interaction that opens it and closes it before its markup is removed. This is local presentation; no broader feature lifecycle is needed. ## Match the control to the View lifecycle With normal View monitoring, `onDomRemove` runs before an attached View re-renders or detaches. Closing there releases the modal while its element is still connected. `onBeforeDestroy` also covers terminal disposal, including a View already detached. Cleanup can run more than once; closing an already closed dialog does nothing. For an external editor, chart, or other package, follow that package's actual API instead of treating the dialog as its adapter: - Create DOM-independent resources in `initialize`. Bind to freshly rendered elements in `onRender`. - Create or measure controls that require a connected element in `onAttach`, or `onDomRefresh` if they also need updating after an attached render. An adopted, already attached element gets no construction-time attach event; initialize that case explicitly. - Dispose or release bindings before the renderer replaces their elements. Suspend work on detachment if the View can return; re-establish it when attached again. - Release terminal resources in `onBeforeDestroy`, even when the View was never attached. Let the package's ownership rules determine whether disposal may be repeated. Keep the renderer and external control from both updating the same subtree. Use the package's documented change events to update local data or emit user intent, and release its subscriptions or global handlers when its owner ends. See the [View lifecycle reference](/docs/api/view.md#lifecycle-hooks-and-events) for hook arguments and ordering. ## Reuse through a Behavior A Behavior receives its host's render, attach, detach, and destroy notifications, so the same integration hooks can be reused when their assumptions match each host. The host still owns its DOM. Direct `behavior.destroy()` emits no independent destroy notification. If an integration can be removed separately from its host, release its external resources in an override of `destroy` and delegate to `Behavior.prototype.destroy.call(this)`. Make that cleanup repeat-safe; relying only on forwarded `onBeforeDestroy` leaves direct removal uncovered. See [Behavior disposal](/docs/api/behavior.md#destruction-and-nested-behaviors) for the full contract. ## Check the actual browser behavior Open Help and verify focus enters the dialog. Close it with the form button and with Escape. Open it again before rendering, detaching, or destroying the View and verify it closes before removal. A detached View remains reusable: re-show it through the Region and verify Help opens again. `region.empty()` destroys the View; destroy the Region when its owner finishes. Run these modal checks in a browser. JSDOM can check the mounted markup and Marionette ownership transitions, but it does not supply the dialog's modal methods here. See [consumer testing](/docs/guides/testing.md) for testing boundaries. [Canonical source](/docs/markdown/docs/guides/widgets.md) · [Source identity](/docs/manifest.json) --- Document: docs/guides/accessibility-rendering.md Canonical URL: https://marionettejs.com/docs/guides/accessibility-rendering/ Markdown URL: https://marionettejs.com/docs/guides/accessibility-rendering.md Reading SHA-256: 176380a4f946ee37ffa9ae4066741b6bdeeba144db083206139c08ce2f42eabd # Accessibility and rendering Marionette manages View lifetimes and placement. Your templates and interaction design determine accessible names, keyboard behavior, and where focus goes when content changes. ## Start with semantic controls Use a native `button` for an action and a link with `href` for navigation. Label form fields visibly, keep document order meaningful, and preserve a visible focus indicator. A native button supplies keyboard activation, so a delegated `click` handler can serve pointer and keyboard users without duplicating Enter and Space handlers. For a toggle button, keep its label stable and update `aria-pressed`. The [WAI button pattern](https://www.w3.org/WAI/ARIA/apg/patterns/button/) describes keyboard activation and focus behavior. This View owns only local display state. Its class-scoped Lit adapter updates the same template's elements when the user toggles the setting. ```js import { Region, View } from 'marionette'; import LitDomApi from '@mnjs/adapters/dom/lit-html'; import { html } from 'lit-html'; const NotificationSettings = View.extend({ tagName: 'section', createState() { return { title: 'Notifications', muted: false }; }, templateContext() { return this.getState(); }, template: ({ title, muted }) => html`

${title}

${muted ? 'Notifications are muted.' : 'Notifications are enabled.'}

`, ui: { heading: '.heading', mute: '.mute' }, events: { 'click @ui.mute': 'toggleMuted' }, toggleMuted() { const state = this.getState(); state.muted = !state.muted; this.render(); }, focusHeading() { this.getUI('heading')[0].focus(); } }); NotificationSettings.setDomApi(LitDomApi); const mount = document.createElement('main'); document.body.append(mount); const region = new Region({ el: mount }); const settings = new NotificationSettings(); region.show(settings); ``` This is local state, with no persistence claim. When a server-backed setting belongs to a wider feature, give the save and failure workflow to its appropriate owner. The toggle's pressed state conveys the immediate local change; do not add announcements that repeat the control's own feedback without a reason. ## Retain focus during updates; choose it during replacement The toggle does not dismiss its context, so focus should remain on the button. Lit can retain the existing nodes when this View renders the same template again. Changing template branches, removing an element, or choosing an insertion provider that replaces descendants can still remove the focused node. Test the actual rendering behavior you use. Showing a different View in a [Region](/docs/api/region.md#showing-a-view) destroys the current View and removes its content. Marionette does not choose a focus destination. For a user-requested screen transition, show the new View, then focus its heading or an appropriate first control. For example, another `NotificationSettings` instance named `nextSettings` can use `region.show(nextSettings); nextSettings.focusHeading();` after the transition is accepted. The heading's `tabindex="-1"` permits programmatic focus without adding an extra Tab stop. Keep this focus decision with the owner performing the transition. An unrelated background update should not move focus to a new heading. If a dialog closes, returning to its opener may be the appropriate policy instead. Follow the [WAI guidance on predictable focus](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_focus_discernable_predictable) and test the resulting keyboard sequence. ## Keep text and raw HTML boundaries explicit Marionette's native DOM provider inserts template strings with `innerHTML`. It treats a string as HTML, so interpolating untrusted text into that string is unsafe. Use `textContent` for direct text updates or a template system that handles text interpolation. Ordinary string values in Lit child expressions, such as this example's title, become text. See [Lit expressions](https://lit.dev/docs/templates/expressions/#primitive-values). Raw HTML insertion, including Lit's [`unsafeHTML`](https://lit.dev/docs/templates/directives/#unsafehtml), requires trusted or sanitized HTML at the application's boundary. URL attributes also need the application's allowed-URL policy; text interpolation is not a complete security policy. Choose an integration whose output matches the [renderer and DOM provider](/docs/api/providers/dom.md) contract. Keep renderer-owned nodes under that renderer's control rather than overwriting their contents independently. ## Verify the user-visible behavior Use browser interaction tests to Tab to the button, activate it with Enter and Space, and check both `aria-pressed` and visible feedback. Confirm the same button remains focused through the local update. Then replace the View through its Region, verify the old View is destroyed, and check that the transition owner moves focus to the intended new destination. Include a text value containing HTML-like characters and verify it stays text. Test with keyboard navigation and assistive technology for your complete application; these focused checks do not establish accessibility conformance. See [consumer testing](/docs/guides/testing.md) for test setup and [retained restart](/docs/guides/retained-restart.md) for updates that preserve a larger page. [Canonical source](/docs/markdown/docs/guides/accessibility-rendering.md) · [Source identity](/docs/manifest.json) --- Document: docs/guides/production.md Canonical URL: https://marionettejs.com/docs/guides/production/ Markdown URL: https://marionettejs.com/docs/guides/production.md Reading SHA-256: c488fe9e57e11cb372f7c6dc92ab7f83c2b6c64d530398b7227433c643ee68fa # Build and deploy an application Deploy the application bundle with its configured providers and verify it through the server that will serve users. Marionette manages UI ownership and lifecycle; your bundler and server provide the build and delivery boundaries. ## Keep installed packages coherent These docs target **5.0.0**. Follow the [quick start](/docs/quick-start.md) to install matching package versions; keep the lockfile and use the documentation bundled with that installation. Verify your application against that exact installation before deployment. Use named ESM imports from `marionette` and the documented adapter subpaths. Build against installed packages rather than repository source aliases. Include the template engine required by the chosen adapter. Choose observable data and API access through [setup](/docs/integrations/setup.md). Configure providers before constructing consumers. A shared setup module suits one class family; class setters such as `View.extend(...).setDomApi(LitDomApi)` suit a configured subclass. Configure CollectionView and its child View classes when both consume that adapter. Use an isolated runtime when independently configured features coexist. [Runtime configuration](/docs/api/runtime.md) explains these scopes; changing providers on live instances does not rebuild them. ## Build the browser artifact In the quick-start project, run `npm run build` and deploy the resulting `dist` directory. Preview that output locally with `npx vite preview`; Vite's preview server is for local verification. Configure the bundler's public base when serving from a subdirectory, and check the emitted asset URLs at that location. See Vite's [deployment guide](https://vite.dev/guide/static-deploy.html) and [public base configuration](https://vite.dev/guide/build.html#public-base-path). Run the application's lint and TypeScript checks separately from bundling. The [tooling guide](/docs/tooling.md) supplies installed-package configurations. A successful build does not establish readiness, cleanup, or browser interaction behavior; [consumer tests](/docs/guides/testing.md) exercise those boundaries. ## Handle startup at its owner Put required asynchronous readiness in Application `prepareStart`, pass its signal to cancellable requests, and connect the ready UI in synchronous `onStart`. The caller of `start()` handles rejection and decides whether to show an error, offer retry, or end the feature. Retain the Application so the owner can call synchronous `stop()` for reuse or `destroy()` when its lifetime ends. See the [Application failure contract](/docs/api/application.md#preparation-cancellation-and-failure) for cancellation, partial activation and lifecycle results. Use [retained restart](/docs/guides/retained-restart.md) to repeat preparation while preserving the active page. Create retained shells once and update only the source or child Region that changed. Embedded features follow the [existing UI ownership boundary](/docs/guides/existing-ui.md); their host must end the feature before removing its mounting content. ## Serve direct links deliberately Serve the application and API over HTTPS in production. Relative fetch URLs use the application's origin. For path-based navigation, serve the application entry for recognized document routes, including direct visits and reloads. Keep asset and API handling separate: a missing JavaScript file or API resource must retain its appropriate error response rather than receiving application HTML. Test an unknown document route according to your chosen server/client not-found policy. Fragment routing does not require a separate server path for each fragment. The [navigation guide](/docs/guides/routing.md) covers URL ownership. Publish an entry document with the assets it references. Choose caching rules that allow clients to obtain a coherent build, and check an upgrade from a previously loaded version as well as a clean visit. Include the deployed base path and API origin in those checks. ## Check the deployed application Verify a clean load, direct route reload, navigation, missing asset/API responses, failed readiness and retry, and feature removal while preparation is pending. Exercise keyboard interaction and focus in a real browser. Preserve errors with their stack and Marionette diagnostic code so the [installed lookup](/docs/tooling.md#look-up-a-diagnostic) matches the runtime. The framework's browser test configuration runs Chromium, Firefox and WebKit through Playwright. Its browser transpilation query is `baseline widely available`. Those choices describe the repository's validation and transpilation profile; they do not establish behavior in every browser/device or supply missing browser APIs. Select and test your application's actual browser targets with its renderer, providers and deployed bundle. [Canonical source](/docs/markdown/docs/guides/production.md) · [Source identity](/docs/manifest.json) --- Document: docs/records.md Canonical URL: https://marionettejs.com/docs/records/ Markdown URL: https://marionettejs.com/docs/records.md Reading SHA-256: 9fadc8ee813a1247a2691ee87705082f909b31e94eac42de5d88e8b44af3829c # Build a records feature Build a feature that loads records and shows the selected record beside its list. The [runnable example](/docs/source/examples/records/README.md) also reloads without replacing the layout, retries failures, and closes/reopens the feature. The excerpts below come from that example; follow the source links for the complete modules. ## Run it The source is included with these docs; the commands here use the repository checkout. For an application using installed packages, follow the [quick start](/docs/quick-start.md) and [setup reference](/docs/integrations/setup.md). This repository example uses the pinned Node 24 contributor toolchain. Packaged applications follow the [consumer Node LTS policy](/docs/quick-start.md). From the framework repository root: ```sh npm ci --ignore-scripts npm --prefix examples/records ci --ignore-scripts npm --prefix examples/records run dev ``` Open . Select a record, close the feature, then open it again. Each new start loads records and clears selection. The page's Open/Close controls remain mounted. ## Attach to the page [index.html](/docs/source/examples/records/index.html) contains the page heading, controls, and empty content container. MainApplication's onStart attaches PageView to `#app` with `setView(new PageView({ el }))`. PageView uses `template: false`: its existing markup supplies the DOM, so no render or show call is needed. Its Regions and event triggers work normally. In [page.js](/docs/source/examples/records/src/page.js): ```js import { View } from 'marionette'; export const PageView = View.extend({ template: false, regions: { content: '.js-content' }, triggers: { 'click .js-open': 'open:records', 'click .js-close': 'close:records' }, childViewTriggers: { retry: 'retry:records', reload: 'reload:records' } }); ``` The parent owns that View and the registered Records child. The page lasts for the document's lifetime; child stop/restart leaves it mounted. Stopping the parent destroys PageView and removes its element. Starting this parent again, or starting another parent, requires supplying the page markup again. Inside [MainApplication](/docs/source/examples/records/src/main-application.js), `childApps` registers the feature and `viewEvents` connects the page's intent to its lifecycle. The following is an excerpt from the object passed to `Application.extend(...)`: ```js childApps: { records: RecordsApplication }, viewEvents: { 'open:records': 'openRecords', 'retry:records': 'restartRecords', 'reload:records': 'restartRecords', 'close:records': 'closeRecords' }, onStart() { if (this.getView()) { return; } this.setView(new PageView({ el: this.getOption('el') })); this.openRecords(); }, openRecords() { return this.getChildApp('records').start({ region: this.getView().getRegion('content') }) .catch(error => this.showRecordsError(error)); }, restartRecords() { return this.getChildApp('records').restart() .catch(error => this.showRecordsError(error)); }, closeRecords() { return this.getChildApp('records').stop(); } ``` `showRecordsError` is the failure handler described below. Notice that registration and startup are separate: Open starts the child in the page's content Region; Close stops that child while leaving the page available. ## Prepare, then display [RecordsApplication](/docs/source/examples/records/src/records-application.js) follows three lifecycle steps: 1. `onBeforeStart` marks loading. Initial startup resets selection and shows LoadingView; an active restart retains the current layout and selection. 2. `prepareStart(options, { signal })` awaits the API, and returns a Collection built from the response. 3. `onStart(app, options, collection)` uses that Collection directly. It creates RecordsLayout and its status View only when needed, then calls `showList` and `showDetail` to apply the prepared content. `showList` wires the new list’s intent listeners. `showView` mounts an initially prepared layout; showing the existing layout again leaves it intact. The preparation and commit methods are part of the same Application definition. `recordsApi` comes from [records-api.js](/docs/source/examples/records/src/records-api.js); `Collection` comes from `@mnjs/data`, and the Views come from [records-views.js](/docs/source/examples/records/src/records-views.js). Excerpt from [RecordsApplication](/docs/source/examples/records/src/records-application.js), inside its `Application.extend(...)` definition: ```js async prepareStart(options, { signal }) { const records = await recordsApi.list({ signal }); return new Collection(records); }, onStart(app, options, collection) { this.records = collection; const state = this.getState(); if (!(this.getView() instanceof RecordsLayout)) { const layout = this.setView(new RecordsLayout()); layout.showChildView('status', new RecordsStatus({ model: state })); } this.showList(); this.showDetail(); state.set({ count: collection.length, loading: false, error: '' }); this.showView(); } ``` `start()` remains pending until preparation completes. Closing during preparation cancels that start. Marionette prevents its obsolete result from reaching `onStart`, even when a service ignores cancellation. A superseded start resolves false rather than entering the error path. The Application needs no separate run flag or request controller for this initial load. `start()` opens the feature and is idempotent while running. Reload and Retry call `restart()` to repeat preparation. The page and ready records layout remain mounted during an active restart. Success replaces the list and detail Regions with the prepared records; it does not render the whole layout. Selection follows the same record ID; an absent record shows empty detail. The [retained-restart guide](/docs/guides/retained-restart.md) demonstrates updating an observable source while retaining child View and input identity too. [RecordsLayout](/docs/source/examples/records/src/records-views.js) defines the three mount points used above. Replacing the list or detail leaves this shell in place: ```js export const RecordsLayout = View.extend({ className: 'records', template: () => html`

Records

`, childViewTriggers: { retry: 'retry', reload: 'reload' }, regions: { status: '.js-status', list: '.js-list', detail: '.js-detail' } }); ``` ## Follow one selection In [records-views.js](/docs/source/examples/records/src/records-views.js), a row emits `select` and RecordsList forwards `select:record`. Each row receives the same selection state through `childViewOptions`: ```js const RecordRow = View.extend({ tagName: 'li', modelEvents: { change: 'render' }, stateEvents: { 'change:selectedId': 'render' }, templateContext() { return { selected: this.getState().get('selectedId') === this.model.id }; }, template: ({ title, selected }) => html` `, triggers: { 'click button': 'select' } }); export const RecordsList = CollectionView.extend({ tagName: 'ul', attributes: { 'aria-label': 'Records' }, childView: RecordRow, childViewOptions() { return { state: this.getState() }; }, childViewTriggers: { select: 'select:record' } }); ``` The Application owns a `Model` with `selectedId`, `count`, `loading`, and `error`. Its `stateEvents: { 'change:selectedId': 'showDetail' }` connects a selection change to the detail Region. These methods connect the list's intent and choose the detail. Another excerpt from [RecordsApplication](/docs/source/examples/records/src/records-application.js), inside its `Application.extend(...)` definition: ```js showList() { const state = this.getState(); const list = this.getView().showChildView('list', new RecordsList({ collection: this.records, state })); this.listenTo(list, { 'select:record': row => state.set('selectedId', row.model.id) }); }, showDetail() { const model = this.records.get(this.getState().get('selectedId')); const detail = model ? new DetailView({ model }) : new EmptyDetailView(); this.getView().showChildView('detail', detail); } ``` ```text Row button → select → list's select:record → selectedId changes ├─ rows update selection (via shared state) └─ detail Region shows the selected record ``` The callback receives the emitting row, so it can read `row.model.id`. The Views need no reference to the Application or to one another. The Application chooses DetailView for a selected record or EmptyDetailView when selection is empty. Each View renders one presentation. The list never reaches into the detail markup. Selection keeps the page, list, and rows mounted. ## Keep the boundaries small | Source | Responsibility | | --- | --- | | [main-application.js](/docs/source/examples/records/src/main-application.js) | Own the page and registered Records child; connect controls through `viewEvents` and handle loading failures. | | [records-application.js](/docs/source/examples/records/src/records-application.js) | Prepare records, own their collection and selection, coordinate the ready UI. | | [records-views.js](/docs/source/examples/records/src/records-views.js) | Render loading, error, list, and detail content. Emit intent. Regions and CollectionView manage their child Views. | | [records-api.js](/docs/source/examples/records/src/records-api.js) | Implement `list({ signal })`, returning `{ id, title, description }` objects. | | [page.js](/docs/source/examples/records/src/page.js) | Define the page Regions and Open/Close controls; forward Retry intent from the displayed error View. | | [main.js](/docs/source/examples/records/src/main.js) | Start MainApplication at the root element. | | [setup.js](/docs/source/examples/records/src/setup.js) | Configure the data/state integration and Lit renderer once. | MainApplication owns the page and registers RecordsApplication as a child. Its `viewEvents` connects page intent. `onStart` attaches the page once and starts the child in its content Region. Parent restart retains both without installing listeners or starting the child again. The parent is ready when its page is available; the child has its own data readiness. Stopping or destroying the parent also stops or destroys the registered child. `@mnjs/data` provides observable Models and Collections. See [data setup](/docs/integrations/setup.md#observable-data-and-api-access) for the API boundary. RecordsApplication imports recordsApi directly. There is no configurable service constructor option. The API module returns plain JSON records, and preparation adapts them to the feature-owned Collection. A service that already supplies the chosen observable collection can return it directly. Real transport and backend policies belong behind that boundary. Replacing the observable implementation also requires matching Marionette's DataApi/StateApi contracts. ## Failure and cleanup MainApplication catches a rejected child start or restart and logs the original error. A failed initial start is stopped before ErrorView replaces LoadingView. A failed active restart keeps the records layout and data, with the error shown by its status View. Retry repeats preparation; cancelled calls resolve `false` without showing an error. LoadingView, initial ErrorView, and the ready layout share the page's content Region. Status View reload/retry intent travels through the layout and PageView to MainApplication, which calls the child's `restart()` once. Close calls synchronous `stop()` and empties the child's UI. The page controls remain available. Open after Close loads a new Collection, resets selection, and creates a new records layout. Stop destroys the feature's Views; Marionette automatically removes the Application's listeners to the destroyed list. The Application keeps the prepared collection for selection lookup and passes it to the list. A successful start or restart replaces that reference. These local records have no independent resources requiring a stop hook. The Application and selection Model survive so the feature can reopen; final destruction disposes the owned state. Lit interpolation renders record strings as text; no example-specific escaping helper is needed. ## Check changes Stop the preview server first; these checks start their own server from this checkout. From the framework repository root: ```sh npx playwright install chromium firefox webkit npx playwright test --config examples/records/playwright.config.js --workers=3 npm --prefix examples/records run build ``` The browser checks cover readiness, selection, retained restart and failure, retry, cancellation, close/reopen, and disposal. Review ownership as well as behavior. These checks use linked local package builds; installed candidate-package checks are recorded separately from these linked-source checks. [Canonical source](/docs/markdown/docs/records.md) · [Source identity](/docs/manifest.json) --- Document: docs/tooling.md Canonical URL: https://marionettejs.com/docs/tooling/ Markdown URL: https://marionettejs.com/docs/tooling.md Reading SHA-256: 44771021e17789cc6eb3f12a735ffa34dfba21a670bf998fdd28c74f792df8d8 # Check and debug an application Use these checks in the application created by the [quick start](/docs/quick-start.md). Marionette includes declarations, an ESLint plugin, and a version-matched diagnostic catalog. They check specific contracts; they do not decide where your application's workflows belong. ## Lint JavaScript Install the development tools in your application directory: ```sh npm install --ignore-scripts --save-dev --save-exact eslint@10.11.0 typescript@6.0.3 ``` Create `eslint.config.mjs`: ```js import marionette from 'marionette/eslint'; export default [ { ...marionette.configs.recommended, files: ['**/*.js', '**/*.mjs'], ignores: ['dist/**', 'node_modules/**'] } ]; ``` The recommended configuration enables `marionette/no-private-framework-members`. It reports recognized access to Marionette internals, such as `view._isRendered`, and directs you toward supported APIs such as `view.isRendered()`. It is a JavaScript analysis rule, not a complete type checker: indirect values or dynamic property names can prevent it from identifying a framework member. It does not prohibit your own private fields or determine whether a service call belongs in a View. For example, this `interaction.js` uses public APIs and keeps a local interaction in its View: ```js import { View } from 'marionette'; export const ToggleView = View.extend({ events: { 'click button': 'toggle' }, toggle() { this.trigger('toggle', this); } }); ``` Run the rule against your JavaScript: ```sh npx eslint . --max-warnings=0 ``` Add other ESLint rules according to your application's needs. TypeScript linting also needs a TypeScript parser and configuration; the configuration above covers JavaScript only. ## Check TypeScript against the installed package For a browser application built by Vite or another bundler, create `tsconfig.json`: ```json { "compilerOptions": { "target": "ES2024", "lib": ["ES2024", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "noEmit": true, "skipLibCheck": false, "types": [] }, "include": ["*.ts"] } ``` Adjust `include` to match your source directories. Node applications need configuration suited to their module system. The package supplies its own declarations; do not install declarations for Marionette v4 or alias the package to repository source files. This `screen.ts` checks a Region and View against the installed declarations: ```ts import type { RegionInstance, ViewInstance } from 'marionette'; export function showScreen(region: RegionInstance, view: ViewInstance): void { region.show(view); } ``` Use instance types such as `RegionInstance` and `ViewInstance` in annotations; the `Region` and `View` exports are constructors. Run the compiler independently of the bundler: ```sh npx tsc -p tsconfig.json ``` For example, passing a string to `region.show()` produces a type error because that method expects a View. A successful build may only mean that TypeScript was transformed into JavaScript; run the type check explicitly. Types do not prove that a selector exists, an event happens at the intended time, or a workflow has the right owner. For typed initialization, DOM handlers, state and lifecycle results, follow [Use TypeScript](/docs/guides/typescript.md). ## Look up a diagnostic When an error includes a code such as `MN0003`, use the lookup helper shipped with the same package: ```sh node node_modules/marionette/skills/marionette/scripts/docs.mjs --diagnostic MN0003 ``` The result includes the catalog explanation and any documented recovery guidance. Use the code from the actual error, preserve its stack trace, and read the associated API contract before changing the code. The [agent guide](/docs/agents.md#local-contract-lookup) explains lookup when a package lives in an external store. For runnable interaction, replacement, readiness and teardown tests, follow [Test an application](/docs/guides/testing.md). ## Debug the failing boundary 1. Identify the installed version and configured renderer or data provider. A DOM, template, or persistence problem may belong to that integration. 2. Reproduce the operation with the smallest relevant owner: a local edit in its View, a replacement in its Region, or readiness and shared coordination in its Application. 3. Inspect public state such as `view.isRendered()`, `view.isAttached()`, and `view.isDestroyed()`. Observe lifecycle events instead of reading internal flags. For asynchronous operations, retain and inspect the returned Promise so a failure remains visible. 4. Test the user-visible outcome and teardown. For a replaced panel, verify that the old panel is removed and its owned subscriptions stop reacting. For a failed start, verify the intended error UI and retry behavior. Use browser developer tools for the actual DOM, network, and event timing. Lint, declarations, and diagnostic lookup complement those checks. None measures architectural quality or proves that an agent understood the documentation; use the [ownership guide](/docs/architecture.md) and review observable behavior for that. ## Agent installation The installed package includes `skills/marionette`. The skill routes to that application's installed docs and includes an offline lookup helper. Reading `SKILL.md` directly also works. For a Codex project, copy the complete folder to `.agents/skills/marionette` in your application and invoke `$marionette`. Keep the scripts with the entrypoint. For other clients, use their documented skill location and invocation support. See [Codex skill installation](https://learn.chatgpt.com/docs/build-skills). The `marionette` plugin is distributed through repository marketplaces, separately from the npm package. Its version tracks the loader and connection, independently of the framework. The plugin locates the application's installed package and loads that package's skill; it does not bundle a newer copy of the API instructions. In a Codex client supporting local marketplaces, add a checkout with `codex plugin marketplace add /absolute/path/to/checkout`, then install Marionette using the client's plugin interface. See [OpenAI plugin installation](https://developers.openai.com/plugins/build/plugins). The repository also includes Claude Code and Cursor marketplace manifests. Verify installation in the client you use; a manifest alone does not establish a working installation or marketplace listing. The loader recognizes `backbone.marionette` 4.x as a v4 installation and identifies older releases separately. Keep existing application work on its v4 contracts. For migration, choose the target v5 release and read that release's [migration guide](/docs/guides/migration.md); installing the plugin does not upgrade the application. When both packages are present, select guidance based on the feature's imports. Choose the copied package skill for local lookup without MCP. The plugin can also use the hosted connection, subject to the identity check below. A release without a packaged skill needs its own documentation or exact source revision. ## Hosted documentation MCP The plugin's connection is `https://mcp.marionettejs.com/mcp`. Before searching it, read the `marionette://catalog` resource. Compare both `provenance.packageVersion` and `provenance.sourceRevision` with the application's installed package and `docs-manifest.json`. A matching version alone does not establish the same contract, including when the hosted catalog serves a development candidate. Follow the catalog's `requestIdentity` field for required tool arguments. Pass the exact `version`; for a development candidate, also pass the matching `sourceRevision` on search and read calls, including `search_docs`, `search_sections`, and retrieval of their returned IDs. Do not use a latest-version alias. A reused prerelease version can contain different contracts. A manifest with `sourceDirty: true` includes changes beyond that revision, so its local docs remain authoritative. If either revision is unavailable, the identities differ, or the service is unavailable, use installed Markdown and the local helper. Hosted results do not override the installed artifact's contract. Copying the skill alone requires no hosted connection; disable the bundled server only through your client's supported settings if using the plugin without MCP. [Canonical source](/docs/markdown/docs/tooling.md) · [Source identity](/docs/manifest.json)