One-way state architecture for a Marionette.js app.
npm install marionette.state
bower install marionette-state
git clone git://github.com/Squareknot/marionette.state.git
A Marionette View is a DOM representation of a Backbone model. When the model updates, so does the view. Here is a quick example:
// Region to attach views
var region = new Mn.Region({ el: '#region' });
// Model synced with '/rest-endpoint'
var model = new Backbone.Model({ url: '/rest-endpoint' });
// View will re-render when the model changes
var View = Mn.ItemView.extend({
modelEvents: {
'change': 'render'
}
});
// Create the view
var view = new View({ model: model });
// Fetch the latest data
model.fetch().done(() => {
// Show the view with initial data
region.show(view);
});
// Updating the model later will cause the view to re-render.
model.fetch();
This is great for views that are only interested in representing simple content. Consider more complex, yet quite common, scenarios:
Common solutions:
toJSON to avoid sending those attributes to the server.Each of these solutions works up until a point, but side effects mount as complexity rises: Logic-heavy views, views unreliably reflecting state changes, models doing too much leading to excessive re-renders, accidentally transmitting state data to server on save.
Separating state into its own entity and then maintaining that entity with one-way data binding solves each of these problems without the side effects of other solutions. It is a pattern simple enough to implement using pure Marionette code, but this library seeks to simplify the implementation further by providing a state toolset.
Mn.State allows a view to seamlessly depend on any source of state while keeping state logic self-contained and eliminates the temptation to pollute core content models with view-specific state. Best of all, Mn.State does this by providing declarative and expressive tools rather than over-engineering for every use case, much like the Marionette library itself.
In each of these examples, views are demonstrated without core content models for simplicity. This emphasizes that state management is occurring independently from renderable core content. Adding core content models should be familiar to any Marionette developer.
From time to time, a view needs to support interactions that only affect itself. On refresh, these states are reset. In this example, a transient view spawns it own, also transient, State.
State flow for a simple interactive view:
Solved with Mn.State:
// Listens to view events and updates view state attributes.
var ToggleState = Mn.State.extend({
defaultState: {
active: false
},
componentEvents: {
'toggle': 'onToggle'
},
onToggle() {
var active = this.get('active');
this.set('active', !active);
}
});
// A toggle button that is alternately "active" or not.
var ToggleView = Mn.ItemView.extend({
template: 'Toggle Me',
tagName: 'button',
triggers: {
'click .js-toggle': 'toggle'
},
stateEvents: {
'change:active': 'onChangeActive'
},
// Create and sync with my own State.
initialize() {
this.state = new ToggleState({ component: this });
Mn.State.syncEntityEvents(this, this.state, this.stateEvents, 'render');
},
// Active class will be added/removed on render and on 'active' change.
onChangeActive(state, active) {
if (active) {
this.$el.addClass('is-active');
} else {
this.$el.removeClass('is-active');
}
}
});
var toggleView = new ToggleView();
var appRegion = new Mn.Region({ el: '#app-region' });
appRegion.show(toggleView);
Relatively often, it is convenient for a view to depend on long-lived application state. This example uses authentication status to demonstrate binding a view directly to the state of the application.
State flow for a simple view that depends directly upon long-lived application state:
Solved with Mn.State:
// Listens to application level events and updates app State attributes.
var AppState = Mn.State.extend({
defaultState: {
authenticated: false
},
componentEvents: {
'login': 'onLogin',
'logout': 'onLogout'
},
onLogin() {
this.set('authenticated', true);
},
onLogout() {
this.set('authenticated', false);
}
});
// Alternately a login or logout button depending on app authentication state.
var ToggleAuthView = Mn.ItemView.extend({
template: 'This Button Label Will Be Replaced',
tagName: 'button',
triggers: {
'click': 'loginLogout'
},
appStateEvents: {
'change:authenticated': 'onChangeAuthenticated'
},
// Bind to app State.
initialize(options={}) {
this.appState = options.appState;
this.appChannel = Radio.channel('app');
Mn.State.syncEntityEvents(this, this.appState, this.appStateEvents, 'render');
},
// Button text will be updated on every render and `action` change.
onChangeAuthenticated(appState, authenticated) {
if (authenticated) {
this.$el.text('Logout');
} else {
this.$el.text('Login');
}
},
// Login/logout toggle will always fire the appropriate action.
loginLogout() {
if (this.appState.get('authenticated')) {
Radio.trigger('app', 'logout');
} else {
Radio.request('app', 'login');
}
}
});
var appChannel = Radio.channel('app');
var appState = new AppState({ component: appChannel });
var toggleAuthView = new ToggleAuthView({ appState: appState });
var appRegion = new Mn.Region({ el: '#app-region' });
appRegion.show(toggleAuthView);
Sometimes a view has its own, transient, internal state that is related to long-lived application state. While this particular example doesn't require that layer of indirection to achieve its goal (a Login/Logout button), the goal here is to demonstrate all that is necessary to achieve two tiers of State.
State flow for a simple view that depends indirectly on long-lived application state:
Solved with Mn.State:
// Listens to application level events and updates state attributes.
var AppState = Mn.State.extend({
defaultState: {
authenticated: false
},
componentEvents: {
'login': 'onLogin',
'logout': 'onLogout'
},
onLogin() {
this.set('authenticated', true);
},
onLogout() {
this.set('authenticated', false);
}
});
// Syncs with application State.
var ToggleAuthState = Mn.State.extend({
defaultState: {
action: 'login'
},
appStateEvents: {
'change:authenticated': 'onChangeAuthenticated'
},
initialize(options={}) {
this.appState = options.appState;
this.syncEntityEvents(this.appState, this.appStateEvents);
},
// Called on initialize and on change app 'authenticated'.
onChangeAuthenticated(appState, authenticated) {
if (authenticated) {
this.set('action', 'logout');
} else {
this.set('action', 'login');
}
}
});
// Alternately a login or logout button depending on app authentication state.
var ToggleAuthView = Mn.ItemView.extend({
template: 'This Button Label Will Be Replaced',
tagName: 'button',
triggers: {
'click': 'loginLogout'
},
stateEvents: {
'change:action': 'onChangeAction'
},
// Create and bind to my own State, which is injected with app State.
initialize(options={}) {
this.appChannel = Radio.channel('app');
this.state = new ToggleAuthState({
appState: options.appState,
component: this
});
Mn.State.syncEntityEvents(this, this.state, this.stateEvents, 'render');
},
// Button text will be updated on every render and 'action' change.
onChangeAction(state, action) {
this.$el.text(action);
},
// Login/logout toggle will always fire the appropriate action.
loginLogout() {
this.appChannel.trigger(this.state.get('action'));
}
});
var appChannel = Radio.channel('app');
var appState = new AppState({ component: appChannel });
var toggleAuthView = new ToggleAuthView({ appState: appState });
var appRegion = new Mn.Region({ el: '#app-region' });
appRegion.show(toggleAuthView);
An application with a business layer for handling persistence to a server is just one more step--the addition of an app controller that responds to Radio requests.
State flow for a simple view that depends indirectly on long-lived application state connected to a business service:
Solved with Mn.State:
```js // Listens to application level events and updates state attributes. var AppState = Mn.State.extend({ defaultState: { authenticated: false },
componentEvents: { 'login': 'onLogin', 'logout': 'onLogout' },
onLogin() { this.set('authenticated', true); },
onLogout() { this.set('authenticated', false); } });
// App controller fields application level requests and t
$ claude mcp add marionette.state \
-- python -m otcore.mcp_server <graph>