5.0.0-beta.1 · Published beta · b06750c5. Published on npm. Match your installed version.
ConsumerTask recipes#
Start with the resource that must survive or be cleaned up. These recipes use Marionette ownership to keep application behavior predictable. Preserve an existing compatible integration; each task identifies when another one is needed.
| Task | Start here | Owner and decision |
|---|---|---|
| Save a draft without losing focus | Forms | The form View owns input DOM and its pending save; update status without rerendering. |
| Change pages while requests overlap | Routing | The application owns URL handling and cancellation; the Region owns the active page. |
| Refresh a root class or ARIA state | Root attributes | Call renderAttributes() when only declared root attributes changed. |
| Keep surviving list rows editable | Collection reconciliation | Keep the observable collection and surviving source objects; do not rebuild the entire CollectionView on every change. |
| Reuse server-provided markup | Prerendered content | Give an existing element to its View; establish child ownership explicitly. |
| Observe a shared model | DataApi | Use the existing provider, or native observable data for a new application; plain objects do not emit changes. |
| React to local owner state | State | Choose StateApi separately from DataApi; use owner cleanup for subscriptions. |
| Wrap a widget that owns DOM | The example below | The View owns the widget handle and tears it down before DOM removal. |
Wrap a DOM-owning widget#
Use this seam for a chart, editor, map, or other widget that renders inside a
Marionette-owned host. The widget factory receives a DOM element and returns a
synchronous destroy() handle. Its own library decides rendering and data updates.
Do not let Marionette and the widget both own the same descendants.
import { View } from 'marionette';
export const WidgetView = View.extend({
template: () => '<div data-widget-host></div>',
initialize({ createWidget }) {
this.createWidget = createWidget;
this.widget = null;
},
onDomRefresh() {
if (!this.widget) {
this.widget = this.createWidget(this.el.querySelector('[data-widget-host]'));
}
},
releaseWidget() {
const widget = this.widget;
this.widget = null;
widget?.destroy();
},
onDomRemove() {
this.releaseWidget();
},
onBeforeDestroy() {
this.releaseWidget();
}
});
Here is a complete factory for trying the ownership contract without installing another library. A real widget adapter supplies the same handle.
import { Region } from 'marionette';
import { WidgetView } from './widget-view.js';
const mount = document.createElement('main');
document.body.append(mount);
const region = new Region({ el: mount });
region.show(new WidgetView({
createWidget(host) {
const button = document.createElement('button');
button.type = 'button';
let count = 0;
button.textContent = 'Count: 0';
const increment = () => { button.textContent = `Count: ${++count}`; };
button.addEventListener('click', increment);
host.append(button);
return {
destroy() {
button.removeEventListener('click', increment);
button.remove();
}
};
}
}));
// When leaving: region.destroy(); mount.remove();
With default lifecycle monitoring, dom:refresh runs after attached rendering
and attachment of rendered content. dom:remove runs before that content is
rerendered or detached. Thus a rerender destroys the previous widget before a new
host appears. Detaching destroys the widget but retains the View; showing that
View again creates a fresh widget. Destruction releases any remaining handle.
Keep monitorViewEvents enabled for this pattern and use Marionette-managed
attachment. Direct append()/remove() calls outside the lifecycle do not become
Marionette attachment events. If the widget must retain expensive state across
navigation, persist that state outside its disposable DOM handle or deliberately
choose a different attachment policy.
The factory must clean up partially acquired resources if initialization throws. An asynchronous widget loader also needs a cancellation/generation check before it attaches; follow the navigation cancellation pattern. A View lifecycle callback does not automatically await arbitrary third-party promises.
The executable fixture checks one widget per attachment, teardown before rerender, detach/reshow, and final destruction. See lifecycle for event ordering.
Preserve an edited row during collection changes#
A stable model object and a stable child View are different from matching IDs in a
new array. For an observable collection, perform the provider's supported
incremental operations. Then verify the unaffected child View and its input node
are the same objects. Avoid calling collectionView.render() after every provider
notification: that explicitly rebuilds children.
If data arrives as an immutable replacement, use a provider/reconciliation policy
that defines how source identity changes are handled. Do not assume trackBy or
ID matching preserves the existing View's model object under every adapter.
The integration guide identifies supported contracts;
testing explains the input identity and stale-subscription assertions
that catch this failure.