0

The problem is that all Object attributes apart from 'name' call the error 'id/url/whatever is not defined' in the console when accessed from the template. A template with just 'name' displays fine and shows the correct name, but as soon as I call a different attribute, eg. id or url, it breaks. The object passed to the view is a parsed static JSON file with all items sitting on the same level and accessible from the console with e.g. collectionName.models[0].get('id');

What has me confused is that the name attribute works, as if it is predefined somewhere in backbone/underscore code as a default.

Am I missing something very obvious? Since I can access the model data from the console, I think that there's something wrong with how the view itself handles the data, but I've tried rewriting it in a couple different ways and nothing seemed to make any difference.


All the relevant code.

Passed object format. This is also what collectionName.models[0].attributes; returns in the console.

[{
"id":"0",
"name": "Building1",
"url": "building_1",
"floors":[{
    "id":"0",
    "name":"Ground Floor",
    "image":"",
    "rooms":[{
        "id": "r_1",
        "name": "Room 1",
    },
    {
        "id": "r_2",
        "name": "Room 2"
    }]
}
}]

}

Example template code:

<span class="name"><%= name %></span>
<%= id %> <%= url %>

The router code:

routes: {
  '': 'intro', // this route is using pretty much identical code and works fine, the model has the exact same format, the only difference is that all attributes work.
  ':id': 'firstLevel'    
},

firstLevel: function  (id) {
  window.singleBuilding = new ThisBuilding({}, {idBuilding: id});

  window.singleBuilding.fetch();      

  this.floorView = new FloorList({
    collection: window.singleBuilding
  });

  var $intro = $('#intro');
  $intro.empty();
  $intro.append(this.floorView.render().el);
}

Views:

window.FloorSingleList = Backbone.View.extend({

  className: 'floor-list',

  initialize: function  () {

  this.template = _.template(tpl.get('floors-list-item')); 
  _.bindAll(this, 'render');
  this.model.bind('change', this.render);
  this.testModel = this.model.attributes; // I tried passing the attributes directly to the templatewithout .toJSON(), which worked exactly the same, as in only the 'name' attribute worked
},

render: function  () {
  console.log("The test data is:", this.testModel);
  console.log("The actual model data is:", this.model);
  var renderedContent = this.template(this.model.toJSON());
  $(this.el).html(renderedContent);

  return this;
 }

});

window.FloorList = Backbone.View.extend({

tagName: 'section',
className: 'intro-list',

initialize: function () {

  this.template = _.template(tpl.get('intro-list'));
  _.bindAll(this, 'render');
  this.collection.bind('reset', this.render, this);
  this.collection.bind('change', this.render, this);
},

render: function  (eventName) {

     var $introList;
     var collection = this.collection;

  $(this.el).html(this.template({ }));
  $introList = this.$('.intro-list');
  collection.each(function (building) {
    var view = new FloorSingleList({
      model: building,
      collection: collection
    });
    $introList.append(view.render().el);
  });

  return this;
}

});

Model code:

window.ThisBuilding = Backbone.Collection.extend({

model : Building,

initialize: function(models, options) {
  // Initialising the argument passed on from the router.
  this.idBuilding = options.idBuilding;
  return this;
},

url : function(){
  return  "data.json"      
},

parse: function (response) {
  console.log("Passed parameters are :", this.idBuilding); // Returns the request parameters passed from the router.
  return response[this.idBuilding];
}

});

Templates & Bootstrap

// templates are loaded during the bootstrap 
tpl.loadTemplates(['header', 'intro-list', 'floors-list-item', 'building-list-item'], function() {
    window.App = new ExampleApp();
    Backbone.history.start();
});

1 Answers1

0

The problem is in how fetch in javascript is asynchronous...

firstLevel: function  (id) {
  window.singleBuilding = new ThisBuilding({}, {idBuilding: id});

  window.singleBuilding.fetch();  // YOU FETCH HERE     

  this.floorView = new FloorList({
    collection: window.singleBuilding
  });

  var $intro = $('#intro');
  $intro.empty();
  $intro.append(this.floorView.render().el); // AND RENDER WHILE YOU CAN'T ASCERTAIN THE FETCH HAS BEEN COMPLETED...
}

So what happens is that the render tries to read a collection that hasn't yet been initialized properly -> your models are not complete yet -> funny readings. Console log works with black magic when regarding to async operations. so it probably tells you something and the reality is something completely different So do this instead:

firstLevel: function  (id) {
  window.singleBuilding = new ThisBuilding({}, {idBuilding: id}); 

  // Don't fetch here...   

  this.floorView = new FloorList({
    collection: window.singleBuilding
  });

  var $intro = $('#intro');
  $intro.empty();
  $intro.append(this.floorView.el); // Don't render here
}

And then in the FloorList-view:

initialize: function () {

  this.template = _.template(tpl.get('intro-list'));
  _.bindAll(this, 'render');
  this.collection.bind('reset', this.render, this);
  this.collection.bind('change', this.render, this);
  this.collections.fetch(); // fetch here, your binds take care of rendering when finished
}

Update 2: Apparently I saw complexity where there was none... Ignore everything below

From Backbone.js docs, on Model.toJSON()

Return a copy of the model's attributes for JSON stringification.

So it returns the attributes as JSON. Now backbone defines id as

A special property of models...

A property, not an attribute. Same goes for url. In a javascript object that represents a backbone Model, the attributes are stored in their own object within the object and the id- and url-properties are stored elsewhere in the model-object. For example the javascript object representing your model could look something like this:

{
  ...
  attributes: Object // this is where your name attribute lives
  ...
  id: 34, // this is your id property
  ...
  __proto__: ctor, // this is where your url-function resides
  ...
}

UPDATE: The id-property is embedded onto the JSON in model.toJSON()

So when you do this.model.toJSON(), you create a JSON from the contents of your model-objects attributes-property and include the id-property. The url -property is not included in this. What you can do for example:

var renderedContent = this.template({
  attributes: this.model.toJSON(),
  url: this.model.url()
});

and in the template

<span class="name"><%= attributes.name %></span>
<%= attributes.id %> <%= url %>

Hope this helps!

jakee
  • 18,486
  • 3
  • 37
  • 42
  • Thanks for the reply! It seems I was quite confusing with the id/url thing. The id and url are also part of the JSON response passed from the model. See updated post for the object structure. The problem I have is with accessing these. I can get the name, but not the id, url or anything else that is not `name`, even though it's sitting right beside it in the attributes. I actually renamed the `name` attribute in JSON file to `nametest` and it came back with `nametest is not defined` even though I could still access it from the console with `collectionName.models[0].get('nametest')`. – Milosz Falinski Jul 31 '12 at 12:36
  • i hoped i saw something else than an async problem, but it was just async problems. If your view depends on something to be fetched to render properly, don't render it anywhere else than after a successful reset or other event from the resource it depends on. – jakee Jul 31 '12 at 13:10
  • Thanks, this is working perfectly! I didn't even consider that this could be the issue, since the 'intro' function fetches data at the same relative point in its' own code. The 'name' attribute working definitely made me look in the wrong places, but I will know better next time. Cheers again. – Milosz Falinski Jul 31 '12 at 13:20