sublime sublime - 5 months ago 37
Javascript Question

Javascript : building an image gallery with backbone

I'm building an image gallery using backbone. User can only view one image at a time, when he navigates next/previous using arrow keys he will see next image in the collection.

I have one top level backbone view called gallery view.

GalleryView = Backbone.View.extend({
initialize: function(options) {
this.collection = PhotosCollection();
}
});


Then for each image I'll create a view

PhotoView = Backbone.View.extend


I'll keep track of current model in collection that is in view, and I'll create photoView for that model.

My question is regarding pagination, or prefetching images that are not in view. I'm not sure about how galleryView can hold multiple PhotoViews and then show the one that is in focus. I'm thinking about building one long div for entire collection and then adding div's for each photo onto it in the indexed position. What would be the strategy of adding removing new images from that div.

Answer

Update

I've modified my original code. Though this is obviously untested it should give you a reasonable start. Images start pre-loading as soon as an img element has been created and the src attribute has been set (as mentioned here). They don't have to be attached to the DOM. This revamped version that I've included below will preload images before and after the current image based on the paginationPadding variable.

End Update

When working with things in Backbone it helps to keep all state related information in your your models and then have the UI respond to it. In this case, you can simply keep track of which image you want to display in a model and then in response to a change in the model tracking what photo you want to view, you can simply re-render the photo gallery to display the desired image. It's reactive.

This is a rough implementation but should hopefully give you some idea of how you might implement a gallery where you can click through the photos like this. Of course, you'd still have a bunch of CSS and such to do. It's incomplete. Let me know if you have any questions.

var StateModel = Backbone.Model.extend({
  defaults: {
    visibleImage: 0
  },

  initialize: function(attributes, options) {
    this.photoCollection = options.photoCollection;
  },

  setIndex: function(photoIndex) {
    if(!this.photoCollection.length) return;

    if(photoIndex < 0) {
      photoIndex = this.photoCollection.length - 1;
    } else if(photoIndex >= this.photoCollection.length) {
      photoIndex = 0;
    }

    this.set('visibleImage', photoIndex);
  },

  setPrev: function() {
    this.setIndex(this.get('visibleImage') - 1);
  },

  setNext: function() {
    this.setIndex(this.get('visibleImage') + 1);
  }
});

var PhotoControls = Backbone.View.extend({
  tagName: 'div',

  initialize: function(options) {
    this.stateModel = options.stateModel;
    this.paginationPadding = options.paginationPadding;
  },

  render: function() {
    var that = this;
    this.$el.empty();

    var ctrlStyle = {cursor: 'pointer', display: 'inline-block', padding: '5px 10px', margin: '0px 5px', border: '1px solid #000'};

    this.$el.append($('<div><< Previous</div>')
      .css(ctrlStyle))
      .click(function() {
        that.stateModel.setNext();
      });

    // Display numbers
    var visibleImage = this.stateModel.get('visibleImage');
    var pgStart = Math.max(visibleImage - this.paginationPadding, 0);
    var pgEnd = Math.min(visibleImage + this.paginationPadding, this.stateModel.photoCollection.length - 1);
    for(var i = pgStart; i <= pgEnd; i++) {
      var $numEl = that.$el.append(
        $('<div>' + (i + 1) + '</div>')
          .css(ctrlStyle)
          .click(function() {
            that.stateModel.setIndex(i);
          });

        if(i == visibleImage) {
          $numEl.css({fontWeight: 'bold', textDecoration: 'underline'});
        }
      );
    }

    this.$el.$('<div>Next >></div>')
      .css(ctrlStyle))
      .click(function() {
        that.stateModel.setPrev();
      });

    return this;
  }
});

var PhotoView = Backbone.View.extend({
  render: function() {
    this.$el.html('<img src="' + this.model.get('url') + '" />');

    return this;
  }
});

var GalleryView = Backbone.View.extend({
  tagName: 'div',

  initialize: function(options) {
    this.paginationPadding = 2;

    this.collection = PhotosCollection();
    this.renderedViews = {};
    this.state = new StateModel(null, {photoCollection: this.collection});
    this.photoControls = new PhotoControls({
      stateModel: this.state,
      paginationPadding: this.paginationPadding
    }).render();
  },

  render: function() {
    if(this.photoView) {
      this.photoView.remove();
    }

    // Pre-fetch images before and after current one based on pagination padding
    var visibleImage = this.stateModel.get('visibleImage');
    var pgStart = Math.max(visibleImage - this.paginationPadding, 0);
    var pgEnd = Math.min(visibleImage + this.paginationPadding, this.stateModel.photoCollection.length - 1);

    for(var i = pgStart; i <= pgEnd; i++) {
      if(!this.renderedViews[fetchModel.cid]) {
        // Images will begin fetching as soon as the 'src' attribute is set on the 'img' element rendered by the view
        this.renderedViews[model.cid] = new PhotoView(this.collection.at(i));
        this.renderedViews[model.cid].render();
      }
      this.$el.html(this.photoView.render().el);
    }

    // Attach the view for the current image
    var renderModel = this.collection.at(this.state.get('visibleImage'));
    if(renderModel && this.renderedViews[renderModel.cid]) {
      this.photoView = this.renderedViews[renderModel.cid];
      this.$el.html(this.photoView.el);
    }

    this.$el.append(this.photoControls.el);

    return this;
  }
});
Comments