0
votes

I'm using Backbone.js and Ruby on Rails to render a collection of posts, but nothing is rendering on screen.

App file

#= require_self
#= require_tree ./templates
#= require_tree ./models
#= require_tree ./views
#= require_tree ./routers

window.IntroProject =
  Models: {}
  Collections: {}
  Routers: {}
  Views: {}

  init: ->
    window.router = new IntroProject.Routers.HomepageRouter({})
    Backbone.history.start()

$ ->
  IntroProject.init()

Model and collection

class IntroProject.Models.Post extends Backbone.Model
  paramRoot: 'post'

class IntroProject.Collections.PostsCollection extends Backbone.Collection
  model: IntroProject.Models.Post
  url: '/'

Router

class IntroProject.Routers.HomepageRouter extends Backbone.Router

  initialize: (options) ->
    @posts = new IntroProject.Collections.PostsCollection()
    @posts.fetch()


  routes:
    '':'index'

  index: ->
    @view ||= new IntroProject.Views.Posts.IndexView(collection: @posts)
    @view.render()

View

IntroProject.Views.Posts ||= {}

class IntroProject.Views.Posts.IndexView extends Backbone.View

  el: '#posts'

  template: _.template( $('#home-post-template').html() ) if $("#home-post-template").length

  render: ->
    @$el.append( @template() )
    @
    # _.each(@collection, (post) =>
    #   postHtml = $(@template(post))
    #   @$el.append( postHtml )
    # )
    # @

Template

  <script type="text/template" id="home-post-template">
  <div class="row">
    <div class="small-12 columns">
      <h2 class="blog-post-title"><%= post.title %></h2>
      <small>
        <p class="blog-post-meta">Published on <%= post.created_at.strftime('%B %d, %Y') %>
          <% if (post.user.email) %>
          by <strong><%= post.user.email %></strong>
          <% end %>
        </p>
      </small>
      <hr>
      <div class="blog-post-body">
        <p><%= post.body.html_safe %></p>
      </div>
    </div>
  </div>
  </script>

I am able to have the render() called when I load the page because I did a console.log('hello'). I even did console.log(@collection) as well. The output:

PostsCollection {models: Array[0], length: 0, _byId: Object, constructor: function, model: function…}
_byId: Object
_idAttr: "id"
length: 2
models: Array[2]
__proto__: ctor

So they the posts are in the collection.

When I do @$el.append( @template() ) it says post is undefined, so I did @$el.append( @template(post: @collection.models). But the post is undefined.

Overall I understand what's happening, but it doesn't work. I'm missing something important. Here's my github repo

Update:

Changes suggested from answer

Router

class IntroProject.Routers.HomepageRouter extends Backbone.Router

  initialize: (options) ->
    @posts = new IntroProject.Collections.PostsCollection()
    @posts.fetch({ reset : true })

  routes:
    '':'index'

  index: ->
    @view ||= new IntroProject.Views.Posts.IndexView(collection: @posts)

View

IntroProject.Views.Posts ||= {}

class IntroProject.Views.Posts.IndexView extends Backbone.View

  el: '#posts'

  template: _.template( $('#home-post-template').html() ) if $("#home-post-template").length

  initialize: ->
    @listenTo(@collection, 'reset', @render)

  render: ->
    _.each(@collection, (post) ->
      postHtml = @template( post )
      @$el.append( postHtml )
    , @)

Template, I removed the post from post.title

Right now I'm getting Uncaught ReferenceError: title is not defined via the console.

2
It might be the @posts.fetch() function. Try implementing a success callback in that function. Example fetch with callback: stackoverflow.com/questions/19713613/… - zer02
Sorry, I just noticed a mistake, it should be @collection.each(function(post){ ... });, and you need to call .toJSON() on your post: postHtml = @template( post.toJSON() ), see this basic example: jsbin.com/musupufa/2/edit?html,js,output - christian314159

2 Answers

0
votes

Your collection is fetched asynchronously, that means by the time you call render on the view, the data is not there yet. You can see that in your console.log(@collection): its length is 0.

Your view needs to listen to its collection and re-render when(ever) it changes/returns from the server. You could add this function to your view class (not that familiar with CoffeeScript thus wrote JS):

initialize : function(){
  this.listenTo(this.collection, 'reset', this.render);    
}

But as the reset event is not explicitly fired when the fetch call returns, change it to this (in your router's setup):

@posts.fetch({ reset : true });

Also, your view's render function doesn't look correct to me, I'd write this

render : function(){
  _.each(this.collection, function(model){
    var html = this.template( model.toJSON() );
    this.$el.append( html );
  }, this);
}

Which also means you'd need to replace the reference to post in your templates:

  • Replace `<%= post.title %>
  • With <%= title %>
0
votes

I was able to find the answer myself with some help. Here's what the fixed code looks like. Omitting if no changes were made

Router

class IntroProject.Routers.HomepageRouter extends Backbone.Router

  initialize: (options) ->
    @posts ||= new IntroProject.Collections.PostsCollection()

  routes:
    '':'index'

  index: ->
    @view ||= new IntroProject.Views.Posts.IndexView(collection: @posts)
    @posts.fetch()

View

IntroProject.Views.Posts ||= {}

class IntroProject.Views.Posts.IndexView extends Backbone.View

  el: 'body'

  template: _.template( $('#home-post-template').html() ) if $('#home-post-template').length

  initialize: ->
    @collection.bind "reset", ->
      @render()
    , @

  render: ->
    @collection.each (post) =>
      $('#posts').append( @template(post.attributes) )
    @

Template

 <script type="text/template" id="home-post-template">
  <div class="row">
    <div class="small-12 columns">
      <h2 class="blog-post-title"><%= post.title %></h2>
      <small>
        <p class="blog-post-meta">Published on <%= post.created_at %>
          <% if (post.user.email) %>
          by <strong><%= post.user.email %></strong>
          <% end %>
        </p>
      </small>
      <hr>
      <div class="blog-post-body">
        <p><%= post.body %></p>
      </div>
    </div>
  </div>
  </script>

The issue was with the fetch and render calls. I was fetching the collection right when the router was initialized. The collection does not persist to the view. By the time I called render on the view, there was nothing to render.

In the solution I now have the fetch() after the view is initialized. Once the view is initialized, then the view would render once the fetch() was successful leaving a reset event to be called. I chose to put el as body because I have events that needed el to be body. Events looks within the el to when to fire.