5.0.0-beta.1 · Published beta · b06750c5. Published on npm. Match your installed version.
ConsumerMarionette.Application#
An Application gives a feature a place to start, stop, restart, and clean up.
It coordinates asynchronous work and child Applications, with an optional
Region for the feature's view tree.
Application includes:
Application is an independent class. It does not inherit from MnObject and
does not add an element or render method.
The Application cidPrefix is mna.
Documentation Index#
- Instantiating An Application
- Application Lifecycle
- Application Ownership
- Application and root View communication
- Application State
- Application Region
- Application Region Methods
Instantiating an Application#
When instantiating an Application there are several properties, if passed,
that will be attached directly to the instance:
channelName, radioEvents, radioRequests, region, regionClass,
stateEvents
import { Application } from 'marionette';
const myApplication = new Application();
Initialization hooks#
preinitialize(options) runs after options and cid are assigned, before
Marionette sets up the Region, Radio, and State. Use it to prepare instance
configuration those steps depend on. initialize(options) runs after that
setup, before State event subscriptions are connected. Owned State is still
created lazily when getState() is first called.
const FeatureApplication = Application.extend({
preinitialize(options) {
this.channelName = options.featureName;
this.region = { el: options.element };
},
initialize() {
// The configured Region and Radio channel are now available.
}
});
Both hooks receive the original constructor arguments and run synchronously;
returned Promises are not awaited. Use onBeforeStart for asynchronous startup
readiness.
Constructor errors propagate to the caller. Marionette does not undo partially completed initialization or automatically release resources from a constructor that throws. See the shared synchronous failure boundary. Application's asynchronous lifecycle has its own cancellation and failure contract, described below.
Application Lifecycle#
start, stop, restart, and destroy return a Promise<boolean>. The
Promise resolves true when the requested target state is reached, including
an idempotent call when that state is already current. It resolves false when
a later incompatible operation supersedes the request. false is cancellation,
not failure. A current lifecycle hook failure rejects its operation Promise.
Compatible repeated calls share the in-flight Promise. Before destruction
begins, the latest incompatible operation wins: for example, stop() during
startup resolves the earlier start() as false, completes the stop lifecycle,
and prevents a stale start event. A start() that supersedes an in-flight
stop waits for the already-running onBeforeStop readiness hook before beginning startup;
it does not emit the invalidated stop completion. Once destruction begins it is terminal;
start() and restart() resolve false, while stop() follows the active
teardown until it has reached a stopped or destroyed state. Completion of an
invalidated asynchronous hook cannot change the Application's running or
destroyed state or emit the invalidated success event.
isRunning() is true only after startup readiness completes and while the
Application is running. It is false before the first start, during lifecycle
transitions, after stop, and after destroy.
Lifecycle operations#
| Current condition | Operation | Lifecycle | Result |
|---|---|---|---|
| Not running | start(options) |
before:start, await readiness, start |
true when running |
| Running | start(options) |
No-op | true |
| Running or starting | stop(options) |
Invalidates startup when needed, then before:stop, stop |
true when stopped; the invalidated start resolves false |
| Stopped | stop(options) |
Empty a root View shown outside startup; otherwise no-op | true |
| Any live, non-destroying state | restart(options) |
Stop when needed, then start | true when running |
| Running or starting | destroy(options) |
Stop when needed, then before:destroy, destroy |
true when destroyed |
| Stopped | destroy(options) |
before:destroy, destroy |
true when destroyed |
| Destroying | repeated destroy() |
Shares the active destroy lifecycle | Same in-flight Promise |
| Destroying | start() or restart() |
Terminal no-op | false |
| Destroying | stop() |
Follows active teardown without interrupting it | true once stopped or destroyed; rejects if teardown fails before stopping |
| Destroyed | start() or restart() |
Terminal no-op | false |
| Destroyed | stop() or destroy() |
Terminal no-op | true |
The onBeforeStart, onBeforeStop, and onBeforeDestroy methods may return a
Promise. Their corresponding before:* events still fire synchronously, but
event-listener return values are not readiness inputs. onStart, onStop,
onDestroy, and their matching events are completion notifications and are not
awaited. A before:* method must not await the same operation whose readiness it
is defining. restart composes the stop and start lifecycles; it does not add a
parallel restart hook path.
Each readiness hook and before:* event receives the Application, the
operation options, and a context object with an AbortSignal:
(application, options, { signal }). When a later operation invalidates
readiness, Marionette aborts its signal before starting replacement readiness.
The signal makes cancellation cooperative; the invalidated operation still
resolves false even when a handler ignores it. When a start, restart, or
destroy operation adopts an in-flight stop phase, it also adopts that phase's
original options and context, and does not abort its signal.
If a replacement start has already canceled the remaining child stops, that
stop phase is no longer adopted. A later stop(), restart(), or destroy()
begins a fresh stop phase with its own options and context.
The context belongs to the readiness phase rather than to one caller's Promise.
Completion methods and events receive only (application, options).
Owned child Applications participate in the same operation. After the owner's
before:start readiness, children start sequentially in registration order
before the owner reaches running and emits start. After before:stop
readiness, children stop in that order before the owner reaches stopped and
emits stop. Restart and destroy compose those same phases.
If a direct child operation supersedes an owner-requested child start or stop,
the owner operation resolves false, retains its prior stable state, and does
not emit its completion event. Children that already reached the requested
state remain there. isRunning() describes that Application, not an aggregate
of every descendant state; callers receiving false can inspect child state
through the public hierarchy. Once owner destruction begins, descendant start
and restart calls resolve false so they cannot interrupt terminal teardown.
Starting an Application#
Once configured, await start(options) before dispatching work that requires a
running Application. The optional argument is passed to the lifecycle methods
and events.
The application below loads a session before showing its root View. The supplied
loadSession({ signal }) function returns a Promise for an object with a
name string. It can use fetch, a cache, or the project's existing data layer.
import { Application, View } from 'marionette';
const SessionView = View.extend({
template: () => '<h1></h1>',
onRender() {
this.el.querySelector('h1').textContent = this.model.name;
}
});
export function createSessionApplication({ el, loadSession }) {
const SessionApplication = Application.extend({
async onBeforeStart(app, options, { signal }) {
const session = await loadSession({ signal });
if (signal.aborted) return;
this.session = session;
},
onStart() {
this.showView(new SessionView({ model: this.session }));
}
});
return new SessionApplication({ region: { el } });
}
Create and start it at the application entry point:
Serve this application and its API over HTTPS in production; relative requests use the application origin.
const app = createSessionApplication({
el: document.querySelector('#root-element'),
async loadSession({ signal }) {
const response = await fetch('/api/bootstrap', { signal });
if (!response.ok) throw new Error(`Session request failed: ${response.status}`);
return response.json();
}
});
const started = await app.start();
if (started) {
// Dispatch work that requires the running feature.
}
Check the readiness signal after asynchronous work and before mutating
application state. Marionette prevents a canceled operation from emitting its
success event, but cannot undo a stale assignment inside application code.
A current loader failure rejects start(); handle it at the application entry
point. Route registration and browser-history startup belong to the router's
owner, outside a feature's restartable onStart hook. See
router integration for per-navigation loading and cancellation.
Application Ownership#
An Application may own named child Applications. Ownership is one-way: an Application locates and controls its children, while children receive required collaborators explicitly. Internal parent references exist only to enforce lifecycle and unlink children safely; upward lookup is not public API.
addChildApp(name, application) registers an existing live,
parentless Application instance under a non-empty string name and returns that
instance. Registration does not construct or implicitly start the child. Use
hasChildApp(name) before constructing a dynamic child when duplicate
allocation matters. Registering the same instance again under its existing
owner and name is an idempotent no-op. A conflicting owner, name, runtime, or cyclic ownership relationship throws
MN0031.
Calls to addChildApp after the owner's destruction begins return the supplied
value without inspecting or adopting it. A child from the same runtime whose
destruction has begun is also returned without registration. Live registrations
require the owner and child to belong to the same Marionette runtime.
const root = new Application();
if (!root.hasChildApp('search')) {
root.addChildApp('search', new SearchApplication());
}
const search = root.getChildApp('search');
search.getName(); // 'search'
root.getChildApps(); // { search }
getChildApps() returns a fresh snapshot. Changing the snapshot does
not change ownership. Child lookup methods are reads; they do not start, render,
or otherwise mutate an Application.
Owner lifecycle options are forwarded to each child. A child failure rejects
the owner operation and leaves the owner in its last committed stable state.
Children that already reached the requested state remain there; retry visits
the same registration order, where completed child operations are idempotent.
An owner transition completes only after every child remains in the requested
stable state. A direct opposing child operation cancels the owner transition,
and superseding the owner from before:start or before:stop prevents the
stale transition from changing any further children.
removeChildApp(name, options) destroys the named child and resolves
with it after destruction. An unknown name resolves with undefined. A child
also removes itself from its parent's child hierarchy when destroyed directly. A
running parent stops its children before before:destroy, then destroys owned
children in registration order and finally emits the parent's destroy
completion. A parent's onBeforeDestroy readiness hook can therefore inspect its
stopped, live children. A stopped parent also stops any child that was
started directly before entering destroy readiness. A concurrent direct child
destroy joins terminal teardown and may remove that child before parent
readiness. If child stop or destroy readiness fails, the parent returns to its
last committed stable state and retains that child so destruction can be retried.
The canonical child-Application pattern is explicit construction followed by ownership registration. Registration means lifecycle ownership; it is not a dormant service registry and it has no per-child lifecycle flags. Put a service that must outlive an Application under a longer-lived owner and pass it to the shorter-lived child as a dependency.
import { Application } from 'marionette';
export const lifecycle = [];
const SearchApplication = Application.extend({
onBeforeStart(app, options) {
lifecycle.push(`search:before:start:${ options.source }`);
},
onStart(app, options) {
lifecycle.push(`search:start:${ options.source }`);
},
onBeforeStop(app, options) {
lifecycle.push(`search:before:stop:${ options.source }`);
},
onStop(app, options) {
lifecycle.push(`search:stop:${ options.source }`);
},
onDestroy() {
lifecycle.push('search:destroy');
}
});
const RootApplication = Application.extend({
onBeforeStart(app, options) {
lifecycle.push(`root:before:start:${ options.source }`);
},
onStart(app, options) {
lifecycle.push(`root:start:${ options.source }`);
},
onBeforeStop(app, options) {
lifecycle.push(`root:before:stop:${ options.source }`);
},
onStop(app, options) {
lifecycle.push(`root:stop:${ options.source }`);
},
onDestroy() {
lifecycle.push('root:destroy');
}
});
export const root = new RootApplication();
export const search = root.addChildApp('search', new SearchApplication());
export const started = await root.start({ source: 'owner' });
export const stopped = await root.stop({ source: 'owner' });
Application and root View communication#
Keep the ownership direction visible. The Application constructs the root View, passes dependencies and initial values down through its options or public methods, and listens to semantic View events for messages back up. The View should not find its Application through DOM ancestry or private ownership fields. Use Radio only when the sender and receiver do not share this direct ownership boundary.
import { Application, View } from 'marionette';
export const refreshes = [];
const DashboardView = View.extend({
initialize(options) {
this.initialStatus = options.initialStatus;
},
template() {
return '<button class="refresh">Refresh</button><p class="status"></p>';
},
events: {
'click .refresh': 'requestRefresh'
},
onRender() {
this.showStatus(this.initialStatus);
},
requestRefresh() {
this.trigger('refresh:requested', this, { source: 'button' });
},
showStatus(status) {
this.el.querySelector('.status').textContent = status;
}
});
const DashboardApplication = Application.extend({
region: '#dashboard',
onStart() {
const view = new DashboardView({ initialStatus: 'Idle' });
this.listenTo(view, 'refresh:requested', this.refreshDashboard);
this.showView(view);
},
refreshDashboard(view, request) {
refreshes.push(request);
view.showStatus('Updated');
}
});
export const dashboard = new DashboardApplication();
await dashboard.start();
export const dashboardView = dashboard.getView();
Application state#
An Application may compose one state source. A supplied
state is borrowed; a createState(options) result is owned. getState()
returns the exact source, and stateEvents are installed through the selected
StateApi after initialize.
Application state persists across stop and restart. Destruction releases its subscriptions, then disposes its owned state source through StateApi. Stateless Applications allocate no source or subscription. Asynchronous startup work must use the readiness context's abort signal before committing values so invalidated startup cannot apply stale changes.
Application Region#
An Application coordinates one root View through a single
region. The region property can be
defined in multiple ways.
import { Application } from 'marionette';
import RootView from './views/root';
const MyApp = Application.extend({
region: '#root-element',
onStart() {
this.showView(new RootView());
}
});
const myApp = new MyApp();
await myApp.start();
The onStart callback synchronously renders and shows RootView.
before:render and render run for its template; before:attach and attach
also run when the Region is attached to a document and lifecycle monitoring is
enabled. start() itself remains asynchronous.
region can also be passed as an option during instantiation.
The Application owns a Region that it constructs from a selector, Region class, or definition object. Passing an existing Region instance instead borrows that host. Stopping the Application empties the Region's current View, including one shown directly through the Region. Destroying the Application also destroys a Region it constructed, but never destroys a borrowed Region.
The Application's View is whatever its Region currently shows. Showing a View
through either app.showView(view) or app.getRegion().show(view) updates what
app.getView() returns. Emptying or detaching the Region leaves no current View
without stopping the Application. Restart removes the current View before
onStart may show a new View. If the Region has no View, stopping the Application
leaves any unmanaged HTML alone.
regionClass#
By default the Region is used to instantiate the Application's region.
An extended Region can be provided to the Application definition to override the default.
import { Application, Region } from 'marionette';
const MyRegion = Region.extend({
isSpecial: true
});
const MyApp = Application.extend({
regionClass: MyRegion
});
const myApp = new MyApp({ region: '#foo' });
myApp.getRegion().isSpecial; // true
regionClass can also be passed as an option during instantiation.
Application Region Methods#
The Marionette Application provides helper methods for managing its attached region.
getRegion()#
Return the current host region object for the
Application, or undefined if none was configured. This synchronous query does
not resolve its element or render a View. The host reference is released when
the Application is destroyed.
showView(view, options)#
Display a View instance in the Region attached to the Application. This runs the
View lifecycle. The Application itself is never passed
to Region#show and does not become renderable.
This method is synchronous and returns the supplied View, forwarding options
to Region#show. Configure a Region before calling it. It does not call
start() or wait for Application readiness. Once destruction begins it returns
the supplied View without displaying or adopting it. A missing element allowed
by allowMissingEl also leaves the View caller-owned; use getView() === view
to check that it was shown.
getView()#
Return the Region's currentView, including a View shown directly through the
Region or before Application startup. Returns undefined when the Region has no
current View or the Application has no Region.