Sharing common view Behavior
While building your views, you'll find that you're repeating some common operations such as displaying modals, handling form validation, or custom form fields with complex behavior. We can try to share this using inheritance:
var BaseFormView = Marionette.LayoutView.extend({
ui: {
save: '.save-button'
},
triggers: {
'click @ui.save': 'save:form'
},
onSaveForm: function() {
this.model.save();
}
});
var PersonForm = BaseFormView.extend({
template: require('./templates/form.html')
});
This will work until you need to override anything that the base view provides
i.e. the ui
or triggers
hashes. If the form is also a modal, we then have
to choose the View to extend, forsaking the other. Clearly we need a better way.
What is a Behavior?
At its most basic level, a Behavior is an object attached to a view that is able to listen, and respond, to events/triggers on the view. It has access to the view object, allowing it to perform modifications if it needs to. Let's take a look at a simple form Behavior:
var FormBehavior = Marionette.Behavior.extend({
ui: {
save: '.save-button'
},
events: {
'click @ui.save': 'saveForm'
},
saveForm: function() {
this.view.model.save({})
}
});
var PersonForm = Marionette.LayoutView.extend({
behaviors: {
form: {
behaviorClass: FormBehavior
}
},
template: require('./templates/form.html')
});
Now, with a slightly different configuration, we've achieved the same effect in
a way that can be shared across all views that contain a .save-button
element.
The behaviors
object is used to map Behaviors to Views with extra options. The
object key is completely arbitrary, here we've decided to call it form
to
reflect its use. We then tell the view which class to use with the
behaviorClass
key. Next we'll examine ways of adding extra information to our
behavior.
Listening to View events
Behaviors are able to listen to events and triggers fired on views, models, and collections. Let's take an example of a behavior that highlights newly created items in collections:
var SuccessBehavior = Marionette.Behavior.extend({
collectionEvents: {
add: 'highlightModel'
},
highlightModel: function(model, collection, options) {
var unhighlight = collection.where({newlyAdded: true});
_.each(unhighlight, function(item) {
item.set('newlyAdded', false);
});
model.set('newlyAdded', true);
}
});
/** Using Bootstrap CSS */
var RowView = Marionette.LayoutView.extend({
tagName: 'tr',
template: require('./templates/row.html'),
modelEvents: {
'change:newlyAdded': 'render'
},
onRender: function() {
if (this.model.get('newlyAdded')) {
this.$el.addClass('success');
}
else {
this.$el.removeClass('success');
}
}
});
var TableView = Marionette.LayoutView.extend({
behaviors: {
successHighlight: {
behaviorClass: SuccessBehavior
}
},
tagName: 'table',
className: 'table',
template: require('./templates/table.html')
});
This behavior can now, when a new item is added to our collection, highlight the table row of the newly added model, like so:
var table = new TableView({
collection: new Collection([
{name: 'Scott', language: 'English', handle: 'scott-w'}
])
});
table.add({name: 'Joanne', language: 'English', handle: 'jdaudier'});
Attaching Multiple Behaviors
Attaching multiple behaviors is easy, just add another key in the behaviors
hash:
var ModalBehavior = Marionette.Behavior.extend({
// ... Handle rendering and tearing down a modal
});
var FormBehavior = Marionette.Behavior.extend({
// ... Handle form and save logic
});
var ModalForm = Marionette.LayoutView.extend({
behaviors: {
modal: {
behaviorClass: ModalBehavior
},
form: {
behaviorClass: FormBehavior
}
}
});
Now we have a form that can be a modal! Another common case is to create a basic View that does what you need, then add a modal version of it like so:
var NoteView = Marionette.LayoutView.extend({
template: require('./template/note.html')
});
var ModalNoteView = NoteView.extend({
// May be needed depending on the modal implementation
template: require('./template/modalnote.html'),
behaviors: {
modal: {
behaviorClass: ModalBehavior
}
}
});
Now we're able to both render a Note and display it in a Modal with minimal added effort.
Adding options to our Behavior
Behaviors can also be customized for each view, taking options to slightly alter their behavior. This could be as simple as defining custom elements to look for, or even specific functions to call.
To add options, we just set them next to our behaviorClass
and we can get them
using the getOption
method on Behavior
:
var HighlightBehavior = Marionette.Behavior.extend({
ui: {
item: '.item-handle'
},
events: {
'click @ui.item': 'highlight'
},
// This could be called via an event
highlight: function() {
this.ui.item.addClass(
this.getOption('highlightClass'));
}
});
var HighlightRedView = Marionette.LayoutView.extend({
behaviors: {
highlight: {
behaviorClass: HighlightBehavior,
highlightClass: 'highlight-red'
}
}
});
Setting default options
In general, we'll have a default value in mind for each option so let's set it
using the defaults
hash:
var HighlightBehavior = Marionette.Behavior.extend({
defaults: {
highlightClass: 'highlight-green'
},
ui: {
item: '.item-handle'
},
events: {
'click @ui.item': 'highlight'
},
// This could be called via an event
highlight: function() {
this.ui.item.addClass(
this.getOption('highlightClass'));
}
});
var HighlightGreenView = Marionette.LayoutView.extend({
behaviors: {
highlight: {
behaviorClass: HighlightBehavior
}
}
});
var HighlightRedView = Marionette.LayoutView.extend({
behaviors: {
highlight: {
behaviorClass: HighlightBehavior,
highlightClass: 'highlight-red'
}
}
});
Our HighlightGreenView
doesn't need to set a highlightClass
as the
HighlightBehavior
has highlight-green
as a default value.
Listening to Model/Collection events
Like views, Behaviors are able to listen to the modelEvents
and
collectionEvents
hashes with their own custom behavior. We could use this to,
for example, highlight a saved form
var FormSaveBehavior = Marionette.Behavior.extend({
ui: {
highlight: '.highlight-field'
},
modelEvents: {
error: 'highlightError',
sync: 'highlightSaved'
},
collectionEvents: {
sync: 'clearHighlights'
},
clearHighlights: function() {
this.ui.highlight.removeClass('highlight-green highlight-red')
},
highlightError: function() {
this.ui.highlight.addClass('highlight-red');
this.ui.highlight.removeClass('highlight-green');
},
highlightSaved: function() {
this.ui.highlight.addClass('highlight-green');
this.ui.highlight.removeClass('highlight-red');
}
})