# 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${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`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${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${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` `, 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: ```htmlWelcome.
`, }).setDomApi(LitDomApi); const AboutView = View.extend({ template: () => html`About this application.
`, }).setDomApi(LitDomApi); const NotFoundView = View.extend({ template: () => html`${body}
`, }).setDomApi(LitDomApi); const ContentView = View.extend({ template: ({ title, body }) => html`${body}
`, }).setDomApi(LitDomApi); const ErrorView = View.extend({ template: ({ message }) => html`${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: () => ` `, 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`${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