Backbone.js - Documentation

What is Backbone.js?

Backbone.js is a lightweight JavaScript framework that provides structure to web applications by giving them models with key-value binding and custom events, collections with a rich API of enumerable functions, views with declarative event handling, and connects it all to your existing API over a RESTful JSON interface. It’s not a full-fledged MVC framework like Angular or React; instead, it offers a structured approach to building client-side applications, leaving many design decisions up to the developer. It excels at organizing complex JavaScript applications and keeping them maintainable.

Why use Backbone.js?

Backbone.js offers several advantages:

Key Concepts: Models, Views, Collections, Routers

Setting up a Backbone.js Project

Setting up a Backbone.js project is straightforward. You need to include the Backbone.js library in your HTML file. This can be done by downloading the library and including it locally or by using a CDN (Content Delivery Network). For example, using a CDN:

<!DOCTYPE html>
<html>
<head>
  <title>My Backbone App</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.4.1/backbone-min.js"></script> </head>
<body>
  <div id="app"></div>
  <script src="app.js"></script> </body>
</html>

Then, create a JavaScript file (e.g., app.js) where you’ll define your models, views, collections, and routers. You’ll likely also use a build system like Webpack or Parcel for larger projects to manage dependencies and optimize the code for production.

Basic Example

This simple example demonstrates a basic Backbone.js application:

// Model
var Task = Backbone.Model.extend({
  defaults: {
    title: '',
    completed: false
  }
});

// View
var TaskView = Backbone.View.extend({
  tagName: 'li',
  template: _.template('<input type="checkbox" <%= completed ? "checked" : "" %> /> <%= title %>'), //Requires underscore.js

  events: {
    'change input': 'toggleCompleted'
  },

  initialize: function() {
    this.listenTo(this.model, 'change', this.render);
  },

  render: function() {
    this.$el.html(this.template(this.model.attributes));
    return this;
  },

  toggleCompleted: function() {
    this.model.save({completed: !this.model.get('completed')});
  }
});

// Collection
var TaskList = Backbone.Collection.extend({
  model: Task
});

//Data
var taskList = new TaskList([
  {title: 'Task 1', completed: true},
  {title: 'Task 2', completed: false}
]);

//Rendering
var taskListView = new Backbone.View({
  el: '#app',
  initialize: function(){
    taskList.each(this.addTask, this);
  },

  addTask: function(task){
    var taskView = new TaskView({model: task});
    this.$el.append(taskView.render().el);
  }
});

Remember to include Underscore.js for templating in this example. This example shows a simple task list; more complex applications would build upon these core concepts to create more sophisticated functionality.

Models

Creating Models

Backbone models are created using the Backbone.Model constructor. You typically extend this constructor to create your custom model classes. The simplest way to create a new model instance is by passing a JavaScript object literal containing the model’s attributes:

var Task = Backbone.Model.extend({}); //Extend Backbone.Model to create your model

var task = new Task({ title: 'Grocery Shopping', completed: false });

console.log(task.get('title')); // Output: Grocery Shopping

Model Attributes

Model attributes are key-value pairs that represent the data associated with the model. They are stored in the model’s internal attributes property. You can access and manipulate attributes using methods like get() and set().

Setting and Getting Attributes

task.set('completed', true); // Set the 'completed' attribute to true

console.log(task.get('completed')); // Output: true

task.set({title: 'Shopping', priority: 'high'}); //Set multiple attributes

console.log(task.attributes); // Output: {title: "Shopping", completed: true, priority: "high"}

Model Events

Backbone models trigger events when their attributes change. You can listen for these events using the on() method. Common events include:

task.on('change', function() {
  console.log('Model changed!');
});

task.on('change:title', function(model, newValue) {
  console.log('Title changed to:', newValue);
});

task.set({title: 'New Title'}); //Triggers both events

Validating Models

You can add a validate() method to your model to perform validation before attributes are changed. The validate() method receives the new attributes as an argument and should return an error message (string) if validation fails. If validation fails, the invalid event is triggered, and the set() operation is not performed.

var Task = Backbone.Model.extend({
  validate: function(attrs) {
    if (!attrs.title) {
      return 'Title is required';
    }
  }
});

var task = new Task();
task.on('invalid', function(model, error){
    console.log('Error:', error);
});

task.set({title: ''}); // Triggers the 'invalid' event.

Default Values

You can specify default values for attributes using the defaults property in your model definition. These default values are used if an attribute is not provided when creating a new model instance.

var Task = Backbone.Model.extend({
  defaults: {
    title: 'Untitled Task',
    completed: false
  }
});

var task = new Task();
console.log(task.get('title')); // Output: Untitled Task

Model Persistence (Saving and Fetching)

Backbone models can persist their data to a server using the save() and fetch() methods. These methods typically interact with a RESTful API using methods like $.ajax() (if using jQuery). You’ll need to configure the url property of your model to point to the server endpoint. The idAttribute property defines the attribute that represents the model’s unique identifier on the server. The default is ‘id’.

var Task = Backbone.Model.extend({
  urlRoot: '/tasks' // Server endpoint
});

var task = new Task({ title: 'New Task' });
task.save().then(function(){
    console.log("Task Saved");
}, function(error){
    console.log("Error Saving:", error);
});

var task2 = new Task({id: 1}); //To fetch an existing task
task2.fetch().then(function(){
    console.log("Task Fetched:", task2.attributes);
}, function(error){
    console.log("Error Fetching:", error);
});

Working with Local Storage

While Backbone doesn’t directly support local storage, you can easily integrate it using a library or by implementing custom methods to handle storage operations within your models. This provides offline capability and persistence even without a server.

URL and ID

Collections

Creating Collections

Backbone collections are created by extending the Backbone.Collection constructor. They manage a set of models and provide methods for working with them as a group. When creating a collection, you typically specify the model property, which defines the type of model the collection should hold.

var Task = Backbone.Model.extend({});

var TaskList = Backbone.Collection.extend({
  model: Task
});

var tasks = new TaskList();

Adding and Removing Models

var task1 = new Task({ title: 'Task 1' });
var task2 = new Task({ title: 'Task 2' });

tasks.add(task1);
tasks.add([task2, new Task({title: 'Task 3'})]); //Adding multiple models

console.log(tasks.length); // Output: 3

tasks.remove(task1); //Removing a model

console.log(tasks.length); // Output: 2

tasks.reset([{title: 'Task A'}, {title: 'Task B'}]); //Resetting the collection

console.log(tasks.pluck('title')); //Output: ['Task A', 'Task B']

Collection Events

Backbone collections trigger events when models are added, removed, or changed. Common events include:

tasks.on('add', function(model) {
  console.log('Model added:', model.get('title'));
});

tasks.on('remove', function(model) {
  console.log('Model removed:', model.get('title'));
});

tasks.add(new Task({ title: 'Task 4' }));

Fetching Collections from a Server

Similar to models, collections can fetch data from a server using the fetch() method. This method typically interacts with a RESTful API to retrieve a list of models. You need to specify the url property for your collection. The server should return a JSON array of model data.

var TaskList = Backbone.Collection.extend({
  model: Task,
  url: '/tasks'
});

var tasks = new TaskList();
tasks.fetch().then(function() {
  console.log('Tasks fetched:', tasks.toJSON());
}, function(error){
    console.log("Error fetching tasks:", error);
});

Filtering and Sorting Collections

//Filtering:
var completedTasks = tasks.filter(function(task) {
  return task.get('completed');
});

//Where:
var highPriorityTasks = tasks.where({ priority: 'high' });

//Sorting:
tasks.comparator = function(task1, task2) {
  return task1.get('title').localeCompare(task2.get('title')); //Sort by title alphabetically
};
tasks.sort();

Searching Collections

While Backbone doesn’t have a dedicated search method, you can easily implement searching using the filter() method or by using a library that provides more advanced search capabilities.

//Simple search:
var searchTerm = 'Shopping';
var searchResults = tasks.filter(function(task) {
  return task.get('title').toLowerCase().includes(searchTerm.toLowerCase());
});

Views

Creating Views

Backbone views are created by extending the Backbone.View constructor. They are responsible for rendering models and collections to the DOM and handling user interactions. When creating a view, you typically specify the el (element) property, which represents the HTML element the view will be attached to. You can also specify the template property (often using a templating engine like Underscore.js) and define event handlers in the events property.

var TaskView = Backbone.View.extend({
  tagName: 'li', //Specify tag name if not using an existing element.
  className: 'task', // Add class names
  template: _.template('<input type="checkbox" <%= completed ? "checked" : "" %> /> <%= title %>'),
  events: {
    'click input': 'toggleCompleted'
  },

  initialize: function(options) {
      this.model = options.model;
      this.listenTo(this.model, 'change', this.render);
  },

  render: function() {
    this.$el.html(this.template(this.model.attributes));
    return this;
  },

  toggleCompleted: function() {
    this.model.save({completed: !this.model.get('completed')});
  }
});

var task = new Task({title: "Test Task", completed: false});
var taskView = new TaskView({model: task, el: $('#task-list li:first')}); // Attach to existing element
var taskView2 = new TaskView({model: task}); //Create new element
$('#task-list').append(taskView2.render().el) //Add created element to dom.

Rendering Views

Rendering a view typically involves populating the view’s element with HTML based on the associated model or collection data. This is often done using a templating engine like Underscore.js. The render() method is a common place to perform rendering. The render method should return this for chainability.

Event Handling in Views

Backbone views use the events property to define event handlers. The events property is a hash map where keys are event strings (e.g., ‘click .button’), and values are function names or functions themselves. These handlers are automatically bound to the view’s element when the view is rendered.

Templating with Underscore.js

Underscore.js provides a simple templating engine that is commonly used with Backbone views. Templates are written using embedded JavaScript code within HTML. The _.template() function compiles a template into a function that can then be called with data to generate HTML.

Managing DOM Elements

Backbone views provide convenient methods for interacting with the DOM:

Delegating Events

Event delegation improves performance by attaching event handlers to a parent element instead of individual child elements. Backbone views automatically support event delegation if you use CSS selectors in the events property (e.g., 'click .button'). This is more efficient than attaching events to each element individually, especially when dealing with many elements or frequently updating the DOM.

View Lifecycle

Backbone views have a lifecycle that includes initialization, rendering, and disposal.

Subviews and Composition

Complex views can be created by composing multiple smaller subviews. This promotes modularity and reusability. You can manage subviews within a parent view’s initialize() method or render() method. Remember to call .remove() on subviews to properly clean up.

var ParentView = Backbone.View.extend({
    initialize: function() {
        this.childView = new ChildView();
        this.listenTo(this.childView, 'someEvent', this.handleChildEvent);
    },
    render: function() {
        this.$el.append(this.childView.render().el);
        return this;
    },
    remove: function(){
        this.childView.remove();
        Backbone.View.prototype.remove.call(this);
    },
    handleChildEvent: function(){
        //Handle events from the childView.
    }
});

var ChildView = Backbone.View.extend({
    //ChildView definition
});

Routers

Creating Routers

Backbone routers are created by extending the Backbone.Router constructor. They are responsible for managing the application’s navigation and URL routing. They connect URLs to specific views and actions.

var AppRouter = Backbone.Router.extend({
  routes: {
    '': 'home',
    'tasks/:id': 'taskDetails'
  }
});

var appRouter = new AppRouter();
Backbone.history.start(); //Starts listening to url changes.

Defining Routes

Routes are defined in the routes property of the router. They are key-value pairs where keys are URL patterns, and values are function names that handle those routes. Backbone uses regular expressions to match routes. The :id in the example above is a route parameter that captures a numerical ID from the URL.

Handling Route Navigation

When a URL matches a route, the corresponding function is executed. This function typically creates or updates views and renders them. The this context within these functions refers to the router instance.

var AppRouter = Backbone.Router.extend({
  routes: {
    '': 'home',
    'tasks/:id': 'taskDetails'
  },

  home: function() {
    console.log('Home route');
    // Create and render home view
  },

  taskDetails: function(id) {
    console.log('Task details route, id:', id);
    // Create and render task details view using id.
  }
});

Passing Data Through Routes

You can pass data through routes using query parameters or by encoding data within the URL itself (though this can become less readable). Query parameters are appended to the URL using a ? followed by key-value pairs.

// URL with query parameters:  /tasks/1?status=completed&priority=high
appRouter.navigate('tasks/1?status=completed&priority=high', {trigger: true});

//Access Query Parameters:
var AppRouter = Backbone.Router.extend({
  routes: {
    'tasks/:id': 'taskDetails'
  },
  taskDetails: function(id) {
    var queryParams = Backbone.history.getFragment().split("?")[1];
    var parsedParams = {};
    if(queryParams){
        queryParams.split("&").forEach(param => {
            let [key, value] = param.split("=");
            parsedParams[key] = value;
        });
    }
    console.log("Query Params:", parsedParams);
  }
});

URL Fragment Management

Backbone routers use the browser’s URL fragment (the part after the #) to manage routes. The navigate() method updates the URL fragment. The trigger: true option in navigate() triggers the associated route handler function, if one exists. Otherwise, only the URL is updated.

appRouter.navigate('tasks/2', {trigger: true}); // Updates URL and triggers route handler.
appRouter.navigate('tasks/3'); // Only updates URL, doesn't trigger the handler.

History Management

Backbone uses the HTML5 History API (if available) to manage routing without the # in the URL. You can check if pushState is available using Backbone.history.started. If the browser doesn’t support the HTML5 History API, Backbone falls back to using the URL fragment. Ensure you set the root of your application properly for correct history management.

Backbone.history.start({ pushState: true, root: '/my-app/' }); //Use HTML5 History if possible and set root for the application

By default, Backbone.history.start() will use hashbang URLs (/#/myroute). Using pushState offers a cleaner URL experience, removing the need for hashbangs, but requires server configuration to handle the requested URLs gracefully. Failure to properly configure your server will result in a 404 error.

Events

Backbone.js provides a powerful event system that allows for communication between different parts of your application. This system is based on the observer pattern, allowing objects to subscribe to and receive notifications about events triggered by other objects. This promotes loose coupling and improves code maintainability.

Backbone Events

Backbone’s event system is built into its core classes (Backbone.Model, Backbone.Collection, Backbone.View, and Backbone.Router). These classes inherit from Backbone.Events, which provides the fundamental event handling methods.

Custom Events

You can define and trigger any custom event names you want. Event names are strings.

var myObject = {};
_.extend(myObject, Backbone.Events);

myObject.on('myCustomEvent', function(data) {
  console.log('My custom event triggered!', data);
});

myObject.trigger('myCustomEvent', { message: 'Hello!' }); //Triggers the event.

Event Propagation

Events triggered on a Backbone object can propagate up the inheritance chain. For example, events triggered on a model are also triggered on its collection if the collection is listening.

var Task = Backbone.Model.extend({});
var TaskList = Backbone.Collection.extend({ model: Task });
var taskList = new TaskList();

var task = new Task({ title: 'My Task' });
taskList.add(task);

taskList.on('change', function(model) {
  console.log('Collection noticed a change');
});

task.on('change', function(model) {
  console.log('Model changed');
});

task.set({ title: 'Updated Task' }); //Triggers 'change' on model AND collection

Listening for Events

Use the on() method (or its alias, bind()) to listen for events. It takes the event name, a callback function, and an optional context (the this value within the callback) as arguments.

myObject.on('anotherCustomEvent', function() {
  console.log('Another custom event triggered!');
}, this); // 'this' will refer to the global scope in this example, unless specified otherwise.

myObject.trigger('anotherCustomEvent');

//Using bind():
myObject.bind('yetAnotherEvent', ()=>{console.log('Yet another event!')});
myObject.trigger('yetAnotherEvent');

You can listen for multiple events at once using a space-separated string:

myObject.on('event1 event2', function(eventName) {
    console.log('Event triggered:', eventName)
});
myObject.trigger('event1');
myObject.trigger('event2');

Triggering Events

Use the trigger() method to trigger an event. You can pass additional arguments along with the event name, and these arguments will be passed to the event listeners.

myObject.trigger('myCustomEvent', 'Argument 1', 'Argument 2');

off Method

Use the off() method (or its alias, unbind()) to stop listening for events. You can remove all listeners for a specific event, all listeners for all events on an object, or a specific listener for a specific event.

// Remove all listeners for 'myCustomEvent':
myObject.off('myCustomEvent');

//Remove a specific listener:
var listener = function(){console.log("I'm a listener")};
myObject.on('myEvent', listener);
myObject.off('myEvent', listener);

// Remove all listeners for all events:
myObject.off();

Remember that off() without arguments removes all listeners, so use with caution. Always prefer targeted removal of specific listeners whenever possible to prevent unexpected behavior.

Working with APIs

Backbone.js provides a flexible mechanism for interacting with APIs through its built-in Backbone.sync method. This method handles the communication between your Backbone models and collections and a remote server.

Making AJAX Requests with Backbone.sync

By default, Backbone.sync uses $.ajax (if jQuery is included) to make AJAX requests to a server. Backbone.sync handles the different HTTP verbs (GET, POST, PUT, DELETE) based on the method being called on the model or collection (fetch, save, create, destroy). The URL for the request is determined by the model’s or collection’s url property.

//Model
var Task = Backbone.Model.extend({
    urlRoot: '/tasks'
});

var task = new Task({title: "My Task"});
task.save().then(function(response){
    console.log("Task saved:", response);
}, function(error){
    console.error("Error saving task:", error);
});

//Collection
var TaskList = Backbone.Collection.extend({
    model: Task,
    url: '/tasks'
});

var tasks = new TaskList();
tasks.fetch().then(function(response){
    console.log("Tasks fetched:", response);
}, function(error){
    console.error("Error fetching tasks:", error);
});

Customizing Backbone.sync

You can customize Backbone.sync to use different HTTP libraries or to add custom logic, such as authentication or error handling. You can override Backbone.sync globally or on a per-model/collection basis.

//Global override:
Backbone.sync = function(method, model, options) {
  // Custom logic for all sync calls
  console.log('Custom sync function called');
  //You can use fetch, save, etc. here with custom implementation.
};

//Per-model/Collection override
var MyModel = Backbone.Model.extend({
    sync: function(method, model, options){
        //Custom sync logic for MyModel
    }
});

Using RESTful APIs

Backbone.js naturally integrates with RESTful APIs. The default behavior of Backbone.sync aligns well with standard REST conventions:

Ensure your server-side API adheres to RESTful principles for seamless integration with Backbone.

Handling Errors

Errors during AJAX requests can be handled using the error callback function within the options object passed to Backbone.sync. You can inspect the error object to determine the cause and react accordingly.

task.save({}, {
  error: function(model, response, options) {
    console.error('Error saving task:', response.responseText);
    //Display user-friendly error message
  }
});

Authentication and Authorization

Authentication and authorization are usually handled by intercepting requests before they reach Backbone.sync. You might implement this using middleware on your server or by creating a custom Backbone.sync function that adds authentication headers or tokens to every request. Libraries such as axios offer features such as interceptors which makes this simpler. Examples below use a hypothetical authToken variable:

//Custom sync with authentication
Backbone.sync = function(method, model, options) {
    var authToken = localStorage.getItem('authToken');
    var headers = {
        'Authorization': 'Bearer ' + authToken
    }
    options.headers = headers;
    return Backbone.ajax(method, model, options);
};

//Or using axios interceptors:
axios.interceptors.request.use(config => {
    config.headers.Authorization = `Bearer ${localStorage.getItem('authToken')}`;
    return config;
});

Remember to handle token expiration and refresh appropriately. Consider using a library that simplifies authentication and authorization management.

Advanced Topics

Backbone.Marionette (optional)

Backbone.Marionette is a popular extension to Backbone.js that provides additional structure and components for building more complex applications. It offers features such as composite views, regions, and application organization patterns (like a controller), which can significantly improve the maintainability and scalability of your Backbone applications. It simplifies structuring your application into reusable components and handling complex view hierarchies. While not strictly required, Marionette is highly recommended for larger projects.

Backbone.Relational (optional)

Backbone.Relational is another popular add-on that enhances Backbone.js by providing support for managing relationships between models. It simplifies the creation and management of relationships such as one-to-one, one-to-many, and many-to-many, which are common in most data models. This improves data integrity and reduces the need for manual handling of these relationships in your code. Again, this is optional but highly recommended for applications with complex data relationships.

Testing with Backbone

Testing is crucial for building robust and maintainable Backbone.js applications. Popular JavaScript testing frameworks like Jasmine, Mocha, and Jest are well-suited for testing Backbone code. You can test models, collections, views, and routers in isolation or as part of an integrated system. Focus on unit testing individual components and integration tests to ensure the components work correctly together. Consider using tools like Sinon.JS for mocking and spying on dependencies during testing.

Debugging Tips

Debugging Backbone applications can be challenging. Here are some useful debugging tips:

Best Practices

Performance Optimization

For large and complex Backbone applications, consider these optimization strategies:

Appendix

Glossary of Terms

Useful Resources

Further Learning

To deepen your understanding of Backbone.js and its capabilities, consider the following:

Remember that Backbone.js is a powerful but flexible framework. The best approach to learning it involves hands-on experience and a willingness to explore its capabilities and associated tools.