Reusable functionality system for views, allowing shared behavior patterns across different view types through composition rather than inheritance.
Behaviors provide a way to share common functionality across views without using inheritance. They can handle events, manage UI elements, and provide additional methods to views.
/**
* Reusable functionality for views through composition
* @param options - Configuration options for the behavior
* @param view - The view this behavior is attached to
*/
const Behavior: {
new (options?: BehaviorOptions, view?: View): Behavior;
extend(properties: object, classProperties?: object): typeof Behavior;
};
interface Behavior {
/** Initialize the behavior (noop method for overriding) */
initialize(): void;
/** jQuery-like DOM query within the associated view */
$(selector: string): JQuery;
/** Destroy the behavior and clean up resources */
destroy(): void;
/** Proxy view properties to the behavior */
proxyViewProperties(): void;
/** Bind UI elements defined in the behavior's ui hash */
bindUIElements(): void;
/** Unbind UI elements */
unbindUIElements(): void;
/** Get a UI element by name */
getUI(name: string): JQuery;
/** Delegate model and collection events */
delegateEntityEvents(): void;
/** Undelegate model and collection events */
undelegateEntityEvents(): void;
/** Reference to the parent view */
view: View;
/** Merged UI hash from behavior and view */
ui: UIHash;
/** Unique identifier for the behavior */
cid: string;
}
interface BehaviorOptions {
/** Hash of UI element selectors */
ui?: UIHash;
/** Hash of DOM event handlers */
events?: EventsHash;
/** Hash of event triggers */
triggers?: TriggersHash;
/** Hash of model event handlers */
modelEvents?: EventsHash;
/** Hash of collection event handlers */
collectionEvents?: EventsHash;
}Usage Examples:
import { Behavior, View } from "backbone.marionette";
// Confirmation behavior for dangerous actions
class ConfirmationBehavior extends Behavior {
ui() {
return {
dangerousButtons: '.js-dangerous'
};
}
events() {
return {
'click @ui.dangerousButtons': 'onDangerousClick'
};
}
onDangerousClick(event) {
event.preventDefault();
const action = event.currentTarget.dataset.action;
const message = `Are you sure you want to ${action}?`;
if (confirm(message)) {
// Trigger the original action
this.view.trigger('confirmed:action', action, event.currentTarget);
}
}
}
// Tooltip behavior
class TooltipBehavior extends Behavior {
ui() {
return {
tooltipTriggers: '[data-tooltip]'
};
}
events() {
return {
'mouseenter @ui.tooltipTriggers': 'showTooltip',
'mouseleave @ui.tooltipTriggers': 'hideTooltip'
};
}
showTooltip(event) {
const text = event.currentTarget.dataset.tooltip;
// Show tooltip logic
console.log('Showing tooltip:', text);
}
hideTooltip() {
// Hide tooltip logic
console.log('Hiding tooltip');
}
}
// View using multiple behaviors
class ProductView extends View {
template(data) {
return `
<h3>${data.name}</h3>
<p>${data.description}</p>
<button class="js-dangerous" data-action="delete" data-tooltip="This will permanently delete the product">
Delete Product
</button>
<button data-tooltip="Edit product details">Edit</button>
`;
}
behaviors() {
return {
confirmation: {
behaviorClass: ConfirmationBehavior
},
tooltip: {
behaviorClass: TooltipBehavior
}
};
}
onConfirmedAction(action, element) {
if (action === 'delete') {
this.model.destroy();
}
}
}
// Usage
const product = new Backbone.Model({
name: 'Laptop',
description: 'High-performance laptop'
});
const productView = new ProductView({ model: product });
document.body.appendChild(productView.render().el);Methods for integrating behaviors with views and managing their lifecycle.
/**
* Initialize the behavior (called automatically during view construction)
* Override this method to add custom initialization logic
*/
initialize(): void;
/**
* Destroy the behavior and clean up resources
* Called automatically when the parent view is destroyed
*/
destroy(): void;
/**
* Proxy specific view properties to the behavior
* Makes view properties accessible directly on the behavior
*/
proxyViewProperties(): void;Usage Examples:
// Behavior with custom initialization and cleanup
class ModelSyncBehavior extends Behavior {
initialize() {
this.syncInterval = setInterval(() => {
if (this.view.model) {
this.syncModel();
}
}, 30000); // Sync every 30 seconds
}
syncModel() {
console.log('Syncing model...');
this.view.model.fetch();
}
onDestroy() {
if (this.syncInterval) {
clearInterval(this.syncInterval);
}
}
}
// Behavior that proxies view properties
class AdvancedBehavior extends Behavior {
initialize() {
// Access view properties directly
console.log('View model:', this.model);
console.log('View collection:', this.collection);
}
someMethod() {
// Can use view properties as if they were behavior properties
if (this.model) {
return this.model.get('someAttribute');
}
}
}Methods for managing UI elements within behaviors, similar to views.
/**
* Bind UI elements defined in the behavior's ui hash
* Merges with view's UI elements
*/
bindUIElements(): void;
/**
* Unbind UI elements from the behavior
*/
unbindUIElements(): void;
/**
* Get a UI element by name from the merged ui hash
* @param name - UI element name
* @returns jQuery object for the UI element
*/
getUI(name: string): JQuery;
/**
* jQuery-like DOM query within the associated view's element
* @param selector - CSS selector
* @returns jQuery object
*/
$(selector: string): JQuery;Usage Examples:
// Behavior with complex UI interactions
class FormValidationBehavior extends Behavior {
ui() {
return {
inputs: 'input, textarea, select',
submitButton: '.js-submit',
errorContainer: '.js-errors'
};
}
events() {
return {
'blur @ui.inputs': 'validateField',
'click @ui.submitButton': 'validateForm'
};
}
validateField(event) {
const $field = this.$(event.currentTarget);
const value = $field.val();
const fieldName = $field.attr('name');
// Validation logic
const isValid = this.isFieldValid(fieldName, value);
if (isValid) {
$field.removeClass('error');
} else {
$field.addClass('error');
}
}
validateForm(event) {
event.preventDefault();
let isFormValid = true;
const errors = [];
this.getUI('inputs').each((index, input) => {
const $input = this.$(input);
const value = $input.val();
const fieldName = $input.attr('name');
if (!this.isFieldValid(fieldName, value)) {
isFormValid = false;
errors.push(`${fieldName} is invalid`);
}
});
if (isFormValid) {
this.view.trigger('form:valid');
} else {
this.showErrors(errors);
}
}
showErrors(errors) {
const $errorContainer = this.getUI('errorContainer');
$errorContainer.html(errors.map(error => `<div class="error">${error}</div>`).join(''));
}
isFieldValid(fieldName, value) {
// Custom validation logic
return value && value.length > 0;
}
}Methods for handling model, collection, and custom events within behaviors.
/**
* Delegate model and collection events defined in behavior options
* Called automatically during behavior initialization
*/
delegateEntityEvents(): void;
/**
* Undelegate model and collection events
* Called automatically during behavior cleanup
*/
undelegateEntityEvents(): void;Usage Examples:
// Behavior that responds to model changes
class ModelWatchBehavior extends Behavior {
modelEvents() {
return {
'change': 'onModelChange',
'change:status': 'onStatusChange',
'sync': 'onModelSync',
'error': 'onModelError'
};
}
collectionEvents() {
return {
'add': 'onModelAdded',
'remove': 'onModelRemoved',
'reset': 'onCollectionReset'
};
}
onModelChange(model) {
console.log('Model changed:', model.changedAttributes());
this.view.render(); // Re-render view on model change
}
onStatusChange(model, status) {
console.log('Status changed to:', status);
this.updateStatusIndicator(status);
}
onModelSync(model) {
console.log('Model synced successfully');
this.showSuccessMessage();
}
onModelError(model, error) {
console.log('Model error:', error);
this.showErrorMessage(error.message);
}
onModelAdded(model, collection) {
console.log('Model added to collection:', model);
}
onModelRemoved(model, collection) {
console.log('Model removed from collection:', model);
}
onCollectionReset(collection) {
console.log('Collection reset with', collection.length, 'models');
}
updateStatusIndicator(status) {
const $indicator = this.$('.status-indicator');
$indicator.removeClass('active inactive').addClass(status);
}
showSuccessMessage() {
this.$('.message').text('Changes saved successfully').addClass('success');
}
showErrorMessage(message) {
this.$('.message').text(`Error: ${message}`).addClass('error');
}
}/** Unique identifier prefix for behaviors */
cidPrefix: 'mnb';
/** Reference to the parent view */
view: View;
/** Merged UI hash from behavior and view */
ui: UIHash;
/** Unique identifier for the behavior instance */
cid: string;// Behavior with configuration options
class DropdownBehavior extends Behavior {
defaults() {
return {
trigger: 'click',
closeOnOutsideClick: true,
animation: 'slide'
};
}
initialize(options) {
this.options = _.defaults(options || {}, this.defaults());
this.setupDropdown();
}
setupDropdown() {
// Configure dropdown based on options
this.bindTriggerEvents();
if (this.options.closeOnOutsideClick) {
this.bindOutsideClickEvents();
}
}
bindTriggerEvents() {
const eventName = `${this.options.trigger} .js-dropdown-trigger`;
this.view.delegateEvents({
[eventName]: this.toggleDropdown.bind(this)
});
}
toggleDropdown() {
// Toggle with configured animation
const $dropdown = this.$('.js-dropdown-content');
if (this.options.animation === 'slide') {
$dropdown.slideToggle();
} else if (this.options.animation === 'fade') {
$dropdown.fadeToggle();
} else {
$dropdown.toggle();
}
}
}
// Usage with configuration
class MenuView extends View {
behaviors() {
return {
dropdown: {
behaviorClass: DropdownBehavior,
trigger: 'hover',
animation: 'fade',
closeOnOutsideClick: false
}
};
}
}// Behaviors can communicate with each other through the view
class TabsBehavior extends Behavior {
events() {
return {
'click .js-tab': 'onTabClick'
};
}
onTabClick(event) {
const tabId = event.currentTarget.dataset.tab;
this.activateTab(tabId);
// Notify other behaviors
this.view.trigger('behavior:tab:changed', tabId);
}
activateTab(tabId) {
this.$('.js-tab').removeClass('active');
this.$(`[data-tab="${tabId}"]`).addClass('active');
}
}
class ContentBehavior extends Behavior {
initialize() {
// Listen to tab changes
this.view.on('behavior:tab:changed', this.showTabContent.bind(this));
}
showTabContent(tabId) {
this.$('.js-tab-content').hide();
this.$(`#${tabId}-content`).show();
}
}
// View using both behaviors
class TabbedView extends View {
behaviors() {
return {
tabs: TabsBehavior,
content: ContentBehavior
};
}
}interface BehaviorHash {
[name: string]: BehaviorDefinition;
}
interface BehaviorDefinition {
behaviorClass: typeof Behavior;
[option: string]: any;
}