Building Backbone.js apps with Backlift Part 2: Collections and Forms

Cole Krumbholz

by Cole Krumbholz

Mar 5, 2013


This is part two of the Building Backbone.js with Backlift tutorial series. This part will cover:

  • Creating Backbone.js Models and Collections
  • Fetching collections from a backend API
  • Saving data to a backend using a form

During this tutorial we'll create a Pinterest inspired gallery website that showcases photos from Dropbox and allows anyone to edit the captions. We'll be building on the concepts covered in Part 1 of this tutorial which covered setting up a Backbone.js project on Backlift, creating Backbone Views and using Handlebars templates. Again we'll be using Backlift.com to host the website and provide a database for storing and retrieving metadata about the photos in our gallery.

While following this tutorial it's good to have a browser window open to the Backbone.js docs so you can dig deeper into backbone as needed. Similarly, if questions about Backlift come up while you're working through this tutorial, please check out the Backlift docs.

Setting up the project

Let's start with a rough sketch of our gallery using just static HTML and CSS. Then as we work through the tutorial, we'll swap in Backbone.js Models, Views and Templates to create a dynamic website. Download the static version into your Dropbox by clicking the link below:

Here's what you should see in your Dropbox/Apps/Backlift/gallery folder:

.
├── README.md
├── app/
│   ├── main.js
│   ├── satinweave.png
│   └── style.css
├── config.yml
├── index.html
├── libraries/
├── photos/
└── thumbnail.jpg

The project folder structure is similar to the poem app from Part 1 with the addition of a photos folder that contains lots of cat pictures. index.html contains a static list of the photos:

      <h1> My Gallery </h1>
      <div id="gallery">
        <div class="photo"><img src="/photos/cat1.jpg"><p>A cat!</p></div>
        <div class="photo"><img src="/photos/cat2.jpg"><p>A cat!</p></div>
        <div class="photo"><img src="/photos/cat3.jpg"><p>A cat!</p></div>
        <!-- ... -->
      </div>

This gallery website looks cute but right now it's static. If we want to add new photos, we have to change the HTML by hand. Wouldn't it be nice if we could add a photo to the photos folder and see it in the gallery automatically? Let's make it so!

Using the Backlift table of contents API

In order to update our gallery automatically we need a data source that tells us what files are in the photos folder. Luckily Backlift provides a table of contents (TOC) API. This API reports the files in a path specified in the URL. For example, to get a listing of files in the photos folder, we just send a GET request to /backlift/toc/photos. You can test this out by navigating to your gallery website's URL, opening the developer console, and typing:

$.get("/backlift/toc/photos")

The result should be an object with a responseText property containing a JSON document that looks something like this:

[{"url": "/photos/cat1.jpg", 
  "file": "cat1.jpg", 
  "modified": "2013-02-21T12:51:09Z", 
  "created": "2013-02-21T12:51:09Z"}, 
 {"url": "/photos/cat2.jpg", 
  "file": "cat2.jpg", 
  "modified": "2013-02-21T12:51:09Z", 
  "created": "2013-02-21T12:51:09Z"}, 
 {"url": "/photos/cat3.jpg", 
  "file": "cat3.jpg", 
  "modified": "2013-02-21T12:51:09Z", 
  "created": "2013-02-21T12:51:09Z"},
 // ...
]

We can use wildcards in the URL to search for certain files. For example, if we only want a list of .jpg files in the photos folder, we can send a GET request to /backlift/toc/photos/*.jpg.

In the following steps we will use the data from the TOC API to populate a Backbone Collection.

Fetching Data with a Backbone Collection

In a Backbone app, Models are the "things" that your app manipulates, and Collections are groups of things. In our case we want to manipulate a Collection of photos, so each photo will be a Model. By using Models and Collections we can avoid putting data manipulation logic into our Views. Plus, Models and Collections provide convenience methods for working with a backend, and can automatically signal Backbone Views when the data changes. The bulk of the Backbone docs are devoted to Models and Collections.

To create a Photos collection edit your apps/main.js file so that it looks like this:

var Photos = Backbone.Collection.extend({
  url: "/backlift/toc/photos"
});

function main() {
  var photos = new Photos();
  photos.fetch({update: true});
  photos.on("add", function(photo) {
    console.log("fetched "+photo.get('file'));
  });
}

// run main() on page load
$(main);

In the above code we first create a new Photos Collection with a url that refers to the Backlift TOC API. Then we define a main() function that will be called on page load. This function first creates a new photos object that inherits from the Photos Collection and then uses it to fetch() a list of photos.

About Collection urls: Think of the url as a connection to a server that the Collection is "plugged into." Calling fetch() on the Collection will use this connection to retrieve data. Alternatively, if you are familiar with REST, you can think of the url as a resource. Any fetch(), save(), create() or destroy() operations on the Collection or its Models are translated into GET, PUT, POST and DELETE requests to that resource.

Where's the Model? Since we haven't explicitly specified a Backbone Model for our Photos class, Backbone will use the default Backbone.Model. Often the default Model's functionality is sufficient.

The last statement in main() sets up an event listener to notify us each time a new photo is added:

  photos.on("add", function(photo) {
    console.log("fetched "+photo.get('file'));
  });

We need this statement in order to see the results of the fetch() call. Since fetch() is asynchronous, we use the event listener above to schedule a function to be executed when fetch() receives each photo. The add event is one of several events that can be triggered when Models or Collections change asynchronously.

At this point navigate to your gallery website and open the developer console. You should find a listing of each photo in the photos folder like this:

fetched cat1.jpg                main.js:9
fetched cat2.jpg                main.js:9
fetched cat3.jpg                main.js:9
fetched cat4.jpg                main.js:9
fetched cat5.jpg                main.js:9
fetched cat6.jpg                main.js:9
fetched notcat.jpg              main.js:9

If you delete a photo, the page will refresh and the new list will reflect the missing photo. However, since our HTML is still static, the website will show a broken image link. Let's fix that!

Refresher on Views and Templates

In order to correctly display the images in our photos folder, let's create a GalleryView to render each photo using a Handlebars template. This is largely an application of the views and templates concepts that we covered in part 1. First create a new template file in the app folder named photo.handlebars containing the following code:

<div class="photo">
    <img src="{{photo.url}}">
    <p>A cat!</p>
</div>

Then, below the Photos Collection definition in app/main.js create a GalleryView like so:

var GalleryView = Backbone.View.extend({
  initialize: function() {
    this.collection.on("add", this.renderPhoto, this);
  },
  renderPhoto: function(photo) {
    var params = {
      photo: photo.toJSON()
    };
    this.$el.append(Handlebars.templates.photo(params));
    return this;
  }
});

In the initialize function above we've added a listener that responds to add events by calling renderPhoto. The renderPhoto function is passed the new photo model which it first converts into a simple javascript object using photo.toJSON(). Then this object is passed to the photo template to render an HTML snippet that is appended to the el node.

Now change the main function in app/main.js to look like this:

function main() {
  var photos = new Photos();
  var gallery = new GalleryView({
    collection: photos,
    el: $("#gallery")
  });
  photos.fetch({update: true});
}

The main function begins by creating a Photos collection and passing it to a new GalleryView. We also set the GelleryView's el attribute to the #gallery node where our photos will be appended. Finally we call fetch to retrieve the list of photos from the Backlift TOC API. Since the GalleryView.initialize method sets up a listener, we no longer need to do that in main.

What's that {update: true} stuff? Backbone gives you a lot of control over how the fetch() method behaves. By default, fetch() simply resets the collection with the new data from the server, triggering only the reset event. In our case, since we want the add event to be triggered for each new object, we need to set the update flag. See more about customizing fetch() behavior here.

As a final step, we can remove the list of static photos from the gallery div in index.html. This is the same node that we set to our GalleryView's el. Our index.html <body> tag should now look like this:

  <body>
    <div class="container">
      <h1> My Gallery </h1>
      <div id="gallery">
        <!-- GalleryView will fill in this div -->
      </div>
    </div>   
  </body>

Our gallery website should now only contain the images from our Dropbox. If we copy or delete photos from the photos folder in Dropbox, we should see the gallery website update accordingly.

Errors while we work While performing the steps above, our gallery website may have refreshed several times. Until our work was complete, these refreshes may have left the website in various error states. This is to be expected, and useful, since the errors can help us remember what still needs to be done.

Adding a Form to the Photo Template

Currently our photos all have the same caption, "A cat!". However, some of the photos are of non-cats. So, for the last half of this tutorial, we'll make it possible for anyone to edit a photo's caption to provide a more appropriate description.

Let's start by downloading an update to the gallery application to make sure we're on the same page.

In this update I've done a bit of reorganization. I've split the main.js file into three files, main.js, collections.js and views.js. This makes our code easier to maintain, as opposed to stuffing everything into one javascript file. Your new app folder should look like this:

app
├── collections.js
├── main.js
├── photo.handlebars
├── satinweave.png
├── style.css
└── views.js

As our project grows, we need some way to keep our variables from cluttering up the global namespace. So in this update I've added the App object and defined all functions and classes as properties of this object. If you look at line 1 in app/main.js you'll find a line like this at the top:

App = this.App || {};

This imports the App object if it's defined, otherwise it creates a new App object. Once the object is imported, I can attach functions and classes to it. Then I can refer to those functions and classes in other files where the App object is imported. This technique is a good alternative to defining variables on the top-level window object since that object can quickly become overcrowded with properties that could be accidentally overwritten.

Now let's get to work on making the captions editable. We're going to create a text input form at the bottom of each photo. The default value for the input will be "A cat!" however the value can be changed by website visitors to provide a more descriptive caption.

Open up the app/photo.handlebars file and edit it so that it looks like this:

<div class="photo">
  <img src="{{photo.url}}">
  <form class="form-inline" id="{{photo.file}}">
    <input type="text" placeholder="A cat!" value="{{caption.text}}">
  </form>  
</div>

Above we've replaced the <p> with a form containing a single input. Since the list of photos we receive from the TOC API doesn't contain caption data, we've added a separate caption.text expression to the template. Our next step is to create the captions, and pass the data to this template.

But before that, if you open up the gallery website, you can now click on the caption and type in a new one. However if you reload the page, all your caption data is gone. That's because we still need to save the form data back to the Backlift server.

Creating the Captions Collection

In order to save the form data to the Backlift server we must first create a separate captions Collection. In app/collections.js add a new Collection called Captions like so:

App.Captions = Backbone.Collection.extend({
  url: "/backliftapp/captions",
  forFile: function(file) {
    return this.find(function(item) {
      return item.get('file') == file;
    });
  }
});

The Captions collection lets us store a caption for each file in our Photos collection. It includes the forFile() helper function to let us match captions to photos. This function calls the Caption collection's find method to iterate through each caption looking for the one whose file property matches the file argument that we passed in. A typical use case for this method would be, when rendering a particular photo, to first find the caption that matches the photo's file property.

We could have just placed the logic for iterating through the Captions collection directly in the renderPhoto function of GalleryView. However, if the captions collection were to change we'd need to hunt down this code and make sure it still works. Keeping this code within the Captions collection allows Captions to be modular-- contained in one place and easily modified.

Note: We use the special url prefix "/backliftapp" to store and retreive custom app data. Backlift data persistence API endpoints take the form /backliftapp/<collection> where <collection> can be any collection name we want. We don't need to tell Backlift in advance that we're creating a new collection-- as soon as we try to fetch or store data with a persistence API endpoint, Backlift will create the collection. For more information about the persistence API, see here.

Now, in app\main.js we need to create an instance of the new Captions collection and hand it off to the GalleryView. In the past we used a listener to reload the view asynchronously when the photo data was fetched. However, now we have two collections to load. If we use another listener, we may end up rendering the gallery twice. Instead we'll define a function that takes a list of collections, fetches each one from the server, and waits for all of them to load before continuing.

Before the main() function in app\main.js add this new function:

function withCollections(collections, fn) {
  _.each(collections, function(col) {
    col.fetch({
      success: function() {
        col.fetched = true;
        var done = _.every(collections, function(item) {
          return item.fetched == true;
        });
        if (done) {
          fn.call();
        }
      }
    })
  });
}

withCollections() fetches a list of collections, and when all fetch operations have succeeded, it calls a function. Now we can re-write main() to look like this:

function main() {
  App.photos = new App.Photos();
  App.captions = new App.Captions();

  App.gallery = new App.GalleryView({ 
    collection: App.photos,
    captions: App.captions,
    el: "#gallery"
  });

  withCollections([App.photos, App.captions], function() {
    App.gallery.render();
  });
}

Here we create the two Collections and pass them to the GalleryView, similar to the way it was done before. Then we use withCollections() to load the two collections, wait until they're ready, and then render the gallery.

Note: In App.gallery above, we assign App.photos to a generic collections property, and App.captions to a specific captions property. Why not use captions and photos properties? This is because Backbone.Views recognize the collections property and assign it to the top-level collections property on the view. So later in a render method we can access it through this.collection. For the captions, since Backbone.View doesn't know about the captions property we used in construction, we must use a more convoluted way to find that property in the render method: this.options.captions.

Finally we need to update the GalleryView class to use the new caption data. Open app/views.js and edit the GalleryView class to look like this:

App.GalleryView = Backbone.View.extend({
  render: function() {
    var self = this;
    self.$el.empty();
    self.collection.each(function(photo) {
      self.renderPhoto(photo);
    });
    return self;
  },
  renderPhoto: function(photo) {
    var caption = this.options.captions.forFile(photo.get('file'));
    var params = {
      photo: photo.toJSON(),
      caption: caption ? caption.toJSON() : ""
    };
    this.$el.append(Handlebars.templates.photo(params));
    return this;
  }
});

In the new GalleryView we've removed the initialize() method since we no longer need to set up an event listener. Instead we've created a render() function that first clears the view's el and then iterates over all the photos in the collection and calls renderPhoto() for each.

renderPhoto() is mostly the same as before except for two changes. First, the function now starts by finding the appropriate caption from the captions collection, using the forFile() helper method. Secondly, it adds the caption to the params object that's used to render the photo template.

To test to see if our Views and Collections are wired up correctly, open up your browser's developer console and type in the following:

App.captions.add({file:'cat6.jpg', text:'A space cat?'}); App.gallery.render()

This should cause the gallery to re-render, displaying a caption for our outer-space cat.

A note about pre-fetching: In a production app, it would not be smart to send several AJAX requests on page load like we're doing in this example. It causes the render time of the page to be quite slow. Instead it's better to embed the data in the HTML rendered by the server, so that all the information necessary to display the page is available immediately. Backlift has a prefetch mechanism that can be used to embed json content from the server into the page on load. I'll cover its use in a future tutorial.

Final Step: Saving Form Data

The last thing we need to do is save the captions to the Backlift server when a user makes a change. Let's add an event handler to our GalleryView that responds to change events on the form inputs. Add the following properties to GalleryView, starting with events:

App.GalleryView = Backbone.View.extend({

  ...

  },
  events: {
    "change input": "captionChange",
    "submit form": "formSubmit",
  },
  captionChange: function(ev) {
    var data = {
      file: $(ev.target).parent().attr("id"),
      text: $(ev.target).val()
    };
    var caption = this.options.captions.forFile(data.file);
    if (caption) {
      caption.save(data);
    } else {
      this.options.captions.create(data);
    }
  },
  formSubmit: function(ev) {
    ev.preventDefault();
    $(ev.target).find("input").blur();
  }
});

The captionChange() function first extracts data from the form. It then checks to see if a caption already exists for this file. If so it saves the data to that caption, otherwise it creates a new caption. The save() and create() methods are how Backbone.js sends data to the server.

The formSubmit() function is only there to make sure the page doesn't attempt to reload when the user submits the form by pressing Return.

At this point, check out the gallery website. You should now be able to update your photo captions, and when you reload the page, your changes should remain. You can also share this page with your friends, and let them comment on photos as well!

Conclusion

In this tutorial we covered a lot of information. Specifically we built an entire Pinterest-like gallery using Backlift to fetch photos and save captions. The final version can be downloaded here:

This gallery app is pretty cool, but it's not done yet. In the third and final part of this tutorial we'll expand on the gallery website by allowing users to comment on photos. This final part will get much more into some of the functionality that Backlift provides to create user accounts and set data permissions. Stay tuned!

If you have any feedback on this tutorial, please send it to cole (at) backlift.com or post it in the comments section. You can follow me at @colevscode. Happy hacking!


Need a Hacker?

gun.io is where top developers find gigs. Bad developers are screened out, so you'll only hire great hackers at gun.io.

Pssst! Hackers, sign up here!


Comments

About gun.io

We match top developers with high-quality freelance and full-time jobs.

Sign up with GitHub and we'll match you with great gigs based on your open-source portfolio!

blog comments powered by Disqus